mirror of
https://github.com/vercel/commerce.git
synced 2025-07-23 04:36:49 +00:00
Monorepo with Turborepo (#651)
* Moved everything * Figuring out how to make imports work * Updated exports * Added missing exports * Added @vercel/commerce-local to `site` * Updated commerce config * Updated exports and commerce config * Updated commerce hoc * Fixed exports in local * Added publish config * Updated imports in site * It's actually working * Don't use debugger in dev for better speeds * Improved DX when editing packages * Set up eslint with husky * Updated prettier config * Added prettier setup to every package * Moved bigcommerce * Moved Bigcommerce to src and package updates * Updated setup of bigcommerce * Moved definitions script * Moved commercejs * Move to src * Fixed types in commercejs * Moved kibocommerce * Moved kibocommerce to src * Added package/tsconfig to kibocommerce * Fixed imports and other things * Moved ordercloud * Moved ordercloud to src * Fixed imports * Added missing prettier files * Moved Saleor * Moved Saleor to src * Fixed imports * Replaced all imports to @commerce * Added prettierignore/rc to all providers * Moved shopify to src * Build shopify in packages * Moved Spree * Moved spree to src * Updated spree * Moved swell * Moved swell to src * Fixed type imports in swell * Moved Vendure to packages * Moved vendure to src * Fixed imports in vendure * Added codegen to saleor * Updated codegen setup for shopify * Added codegen to vendure * Added codegen to kibocommerce * Added all packages to site's deps * Updated codegen setup in bigcommerce * Minor fixes * Updated providers' names in site * Updated packages based on Bel's changes * Updated turbo to latest * Fixed ts complains * Set npm engine in root * New lockfile install * remove engines * Regen lockfile * Switched from npm to yarn * Updated typesVersions in all packages * Moved dep * Updated SWR to the just released 1.2.0 * Removed "isolatedModules" from packages * Updated list of providers and default * Updated swell declaration * Removed next import from kibocommerce * Added COMMERCE_PROVIDER log * Added another log * Updated turbo config * Updated docs * Removed test logs Co-authored-by: Jared Palmer <jared@jaredpalmer.com>
This commit is contained in:
1
packages/spree/src/api/endpoints/cart/index.ts
Normal file
1
packages/spree/src/api/endpoints/cart/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/spree/src/api/endpoints/catalog/index.ts
Normal file
1
packages/spree/src/api/endpoints/catalog/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/spree/src/api/endpoints/catalog/products.ts
Normal file
1
packages/spree/src/api/endpoints/catalog/products.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
44
packages/spree/src/api/endpoints/checkout/get-checkout.ts
Normal file
44
packages/spree/src/api/endpoints/checkout/get-checkout.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { CheckoutEndpoint } from '.'
|
||||
|
||||
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
|
||||
req: _request,
|
||||
res: response,
|
||||
config: _config,
|
||||
}) => {
|
||||
try {
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Checkout</title>
|
||||
</head>
|
||||
<body>
|
||||
<div style='margin: 10rem auto; text-align: center; font-family: SansSerif, "Segoe UI", Helvetica; color: #888;'>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" style='height: 60px; width: 60px;' fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<h1>Checkout not yet implemented :(</h1>
|
||||
<p>
|
||||
See <a href='https://github.com/vercel/commerce/issues/64' target='_blank'>#64</a>
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
response.status(200)
|
||||
response.setHeader('Content-Type', 'text/html')
|
||||
response.write(html)
|
||||
response.end()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
const message = 'An unexpected error ocurred'
|
||||
|
||||
response.status(500).json({ data: null, errors: [{ message }] })
|
||||
}
|
||||
}
|
||||
|
||||
export default getCheckout
|
22
packages/spree/src/api/endpoints/checkout/index.ts
Normal file
22
packages/spree/src/api/endpoints/checkout/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { createEndpoint } from '@vercel/commerce/api'
|
||||
import type { GetAPISchema, CommerceAPI } from '@vercel/commerce/api'
|
||||
import checkoutEndpoint from '@vercel/commerce/api/endpoints/checkout'
|
||||
import type { CheckoutSchema } from '@vercel/commerce/types/checkout'
|
||||
import getCheckout from './get-checkout'
|
||||
import type { SpreeApiProvider } from '../..'
|
||||
|
||||
export type CheckoutAPI = GetAPISchema<
|
||||
CommerceAPI<SpreeApiProvider>,
|
||||
CheckoutSchema
|
||||
>
|
||||
|
||||
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||
|
||||
export const handlers: CheckoutEndpoint['handlers'] = { getCheckout }
|
||||
|
||||
const checkoutApi = createEndpoint<CheckoutAPI>({
|
||||
handler: checkoutEndpoint,
|
||||
handlers,
|
||||
})
|
||||
|
||||
export default checkoutApi
|
1
packages/spree/src/api/endpoints/customer/address.ts
Normal file
1
packages/spree/src/api/endpoints/customer/address.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/spree/src/api/endpoints/customer/card.ts
Normal file
1
packages/spree/src/api/endpoints/customer/card.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/spree/src/api/endpoints/customer/index.ts
Normal file
1
packages/spree/src/api/endpoints/customer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/spree/src/api/endpoints/login/index.ts
Normal file
1
packages/spree/src/api/endpoints/login/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/spree/src/api/endpoints/logout/index.ts
Normal file
1
packages/spree/src/api/endpoints/logout/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/spree/src/api/endpoints/signup/index.ts
Normal file
1
packages/spree/src/api/endpoints/signup/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
packages/spree/src/api/endpoints/wishlist/index.tsx
Normal file
1
packages/spree/src/api/endpoints/wishlist/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
45
packages/spree/src/api/index.ts
Normal file
45
packages/spree/src/api/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { CommerceAPI, CommerceAPIConfig } from '@vercel/commerce/api'
|
||||
import { getCommerceApi as commerceApi } from '@vercel/commerce/api'
|
||||
import createApiFetch from './utils/create-api-fetch'
|
||||
|
||||
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 SpreeApiConfig extends CommerceAPIConfig {}
|
||||
|
||||
const config: SpreeApiConfig = {
|
||||
commerceUrl: '',
|
||||
apiToken: '',
|
||||
cartCookie: '',
|
||||
customerCookie: '',
|
||||
cartCookieMaxAge: 2592000,
|
||||
fetch: createApiFetch(() => getCommerceApi().getConfig()),
|
||||
}
|
||||
|
||||
const operations = {
|
||||
getAllPages,
|
||||
getPage,
|
||||
getSiteInfo,
|
||||
getCustomerWishlist,
|
||||
getAllProductPaths,
|
||||
getAllProducts,
|
||||
getProduct,
|
||||
}
|
||||
|
||||
export const provider = { config, operations }
|
||||
|
||||
export type SpreeApiProvider = typeof provider
|
||||
|
||||
export type SpreeApi<P extends SpreeApiProvider = SpreeApiProvider> =
|
||||
CommerceAPI<P>
|
||||
|
||||
export function getCommerceApi<P extends SpreeApiProvider>(
|
||||
customProvider: P = provider as any
|
||||
): SpreeApi<P> {
|
||||
return commerceApi(customProvider)
|
||||
}
|
82
packages/spree/src/api/operations/get-all-pages.ts
Normal file
82
packages/spree/src/api/operations/get-all-pages.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { GetAllPagesOperation, Page } from '@vercel/commerce/types/page'
|
||||
import { requireConfigValue } from '../../isomorphic-config'
|
||||
import normalizePage from '../../utils/normalizations/normalize-page'
|
||||
import type { IPages } from '@spree/storefront-api-v2-sdk/types/interfaces/Page'
|
||||
import type { SpreeSdkVariables } from '../../types'
|
||||
import type { SpreeApiConfig, SpreeApiProvider } from '../index'
|
||||
|
||||
export default function getAllPagesOperation({
|
||||
commerce,
|
||||
}: OperationContext<SpreeApiProvider>) {
|
||||
async function getAllPages<T extends GetAllPagesOperation>(options?: {
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
}): Promise<T['data']>
|
||||
|
||||
async function getAllPages<T extends GetAllPagesOperation>(
|
||||
opts: {
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
async function getAllPages<T extends GetAllPagesOperation>({
|
||||
config: userConfig,
|
||||
preview,
|
||||
query,
|
||||
url,
|
||||
}: {
|
||||
url?: string
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
query?: string
|
||||
} = {}): Promise<T['data']> {
|
||||
console.info(
|
||||
'getAllPages called. Configuration: ',
|
||||
'query: ',
|
||||
query,
|
||||
'userConfig: ',
|
||||
userConfig,
|
||||
'preview: ',
|
||||
preview,
|
||||
'url: ',
|
||||
url
|
||||
)
|
||||
|
||||
const config = commerce.getConfig(userConfig)
|
||||
const { fetch: apiFetch } = config
|
||||
|
||||
const variables: SpreeSdkVariables = {
|
||||
methodPath: 'pages.list',
|
||||
arguments: [
|
||||
{
|
||||
per_page: 500,
|
||||
filter: {
|
||||
locale_eq:
|
||||
config.locale || (requireConfigValue('defaultLocale') as string),
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const { data: spreeSuccessResponse } = await apiFetch<
|
||||
IPages,
|
||||
SpreeSdkVariables
|
||||
>('__UNUSED__', {
|
||||
variables,
|
||||
})
|
||||
|
||||
const normalizedPages: Page[] = spreeSuccessResponse.data.map<Page>(
|
||||
(spreePage) =>
|
||||
normalizePage(spreeSuccessResponse, spreePage, config.locales || [])
|
||||
)
|
||||
|
||||
return { pages: normalizedPages }
|
||||
}
|
||||
|
||||
return getAllPages
|
||||
}
|
97
packages/spree/src/api/operations/get-all-product-paths.ts
Normal file
97
packages/spree/src/api/operations/get-all-product-paths.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { Product } from '@vercel/commerce/types/product'
|
||||
import type { GetAllProductPathsOperation } from '@vercel/commerce/types/product'
|
||||
import { requireConfigValue } from '../../isomorphic-config'
|
||||
import type { IProductsSlugs, SpreeSdkVariables } from '../../types'
|
||||
import getProductPath from '../../utils/get-product-path'
|
||||
import type { SpreeApiConfig, SpreeApiProvider } from '..'
|
||||
|
||||
const imagesSize = requireConfigValue('imagesSize') as string
|
||||
const imagesQuality = requireConfigValue('imagesQuality') as number
|
||||
|
||||
export default function getAllProductPathsOperation({
|
||||
commerce,
|
||||
}: OperationContext<SpreeApiProvider>) {
|
||||
async function getAllProductPaths<
|
||||
T extends GetAllProductPathsOperation
|
||||
>(opts?: {
|
||||
variables?: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
}): Promise<T['data']>
|
||||
|
||||
async function getAllProductPaths<T extends GetAllProductPathsOperation>(
|
||||
opts: {
|
||||
variables?: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
async function getAllProductPaths<T extends GetAllProductPathsOperation>({
|
||||
query,
|
||||
variables: getAllProductPathsVariables = {},
|
||||
config: userConfig,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
} = {}): Promise<T['data']> {
|
||||
console.info(
|
||||
'getAllProductPaths called. Configuration: ',
|
||||
'query: ',
|
||||
query,
|
||||
'getAllProductPathsVariables: ',
|
||||
getAllProductPathsVariables,
|
||||
'config: ',
|
||||
userConfig
|
||||
)
|
||||
|
||||
const productsCount = requireConfigValue(
|
||||
'lastUpdatedProductsPrerenderCount'
|
||||
)
|
||||
|
||||
if (productsCount === 0) {
|
||||
return {
|
||||
products: [],
|
||||
}
|
||||
}
|
||||
|
||||
const variables: SpreeSdkVariables = {
|
||||
methodPath: 'products.list',
|
||||
arguments: [
|
||||
{},
|
||||
{
|
||||
fields: {
|
||||
product: 'slug',
|
||||
},
|
||||
per_page: productsCount,
|
||||
image_transformation: {
|
||||
quality: imagesQuality,
|
||||
size: imagesSize,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const config = commerce.getConfig(userConfig)
|
||||
const { fetch: apiFetch } = config // TODO: Send config.locale to Spree.
|
||||
|
||||
const { data: spreeSuccessResponse } = await apiFetch<
|
||||
IProductsSlugs,
|
||||
SpreeSdkVariables
|
||||
>('__UNUSED__', {
|
||||
variables,
|
||||
})
|
||||
|
||||
const normalizedProductsPaths: Pick<Product, 'path'>[] =
|
||||
spreeSuccessResponse.data.map((spreeProduct) => ({
|
||||
path: getProductPath(spreeProduct),
|
||||
}))
|
||||
|
||||
return { products: normalizedProductsPaths }
|
||||
}
|
||||
|
||||
return getAllProductPaths
|
||||
}
|
92
packages/spree/src/api/operations/get-all-products.ts
Normal file
92
packages/spree/src/api/operations/get-all-products.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { Product } from '@vercel/commerce/types/product'
|
||||
import type { GetAllProductsOperation } from '@vercel/commerce/types/product'
|
||||
import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { IProducts } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
||||
import type { SpreeApiConfig, SpreeApiProvider } from '../index'
|
||||
import type { SpreeSdkVariables } from '../../types'
|
||||
import normalizeProduct from '../../utils/normalizations/normalize-product'
|
||||
import { requireConfigValue } from '../../isomorphic-config'
|
||||
|
||||
const imagesSize = requireConfigValue('imagesSize') as string
|
||||
const imagesQuality = requireConfigValue('imagesQuality') as number
|
||||
|
||||
export default function getAllProductsOperation({
|
||||
commerce,
|
||||
}: OperationContext<SpreeApiProvider>) {
|
||||
async function getAllProducts<T extends GetAllProductsOperation>(opts?: {
|
||||
variables?: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
}): Promise<T['data']>
|
||||
|
||||
async function getAllProducts<T extends GetAllProductsOperation>(
|
||||
opts: {
|
||||
variables?: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
async function getAllProducts<T extends GetAllProductsOperation>({
|
||||
variables: getAllProductsVariables = {},
|
||||
config: userConfig,
|
||||
}: {
|
||||
variables?: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
} = {}): Promise<{ products: Product[] }> {
|
||||
console.info(
|
||||
'getAllProducts called. Configuration: ',
|
||||
'getAllProductsVariables: ',
|
||||
getAllProductsVariables,
|
||||
'config: ',
|
||||
userConfig
|
||||
)
|
||||
|
||||
const defaultProductsTaxonomyId = requireConfigValue(
|
||||
'allProductsTaxonomyId'
|
||||
) as string | false
|
||||
|
||||
const first = getAllProductsVariables.first
|
||||
const filter = !defaultProductsTaxonomyId
|
||||
? {}
|
||||
: { filter: { taxons: defaultProductsTaxonomyId }, sort: '-updated_at' }
|
||||
|
||||
const variables: SpreeSdkVariables = {
|
||||
methodPath: 'products.list',
|
||||
arguments: [
|
||||
{},
|
||||
{
|
||||
include:
|
||||
'primary_variant,variants,images,option_types,variants.option_values',
|
||||
per_page: first,
|
||||
...filter,
|
||||
image_transformation: {
|
||||
quality: imagesQuality,
|
||||
size: imagesSize,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const config = commerce.getConfig(userConfig)
|
||||
const { fetch: apiFetch } = config // TODO: Send config.locale to Spree.
|
||||
|
||||
const { data: spreeSuccessResponse } = await apiFetch<
|
||||
IProducts,
|
||||
SpreeSdkVariables
|
||||
>('__UNUSED__', {
|
||||
variables,
|
||||
})
|
||||
|
||||
const normalizedProducts: Product[] = spreeSuccessResponse.data.map(
|
||||
(spreeProduct) => normalizeProduct(spreeSuccessResponse, spreeProduct)
|
||||
)
|
||||
|
||||
return { products: normalizedProducts }
|
||||
}
|
||||
|
||||
return getAllProducts
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
export default function getCustomerWishlistOperation() {
|
||||
function getCustomerWishlist(): any {
|
||||
return { wishlist: {} }
|
||||
}
|
||||
return getCustomerWishlist
|
||||
}
|
81
packages/spree/src/api/operations/get-page.ts
Normal file
81
packages/spree/src/api/operations/get-page.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { GetPageOperation } from '@vercel/commerce/types/page'
|
||||
import type { SpreeSdkVariables } from '../../types'
|
||||
import type { SpreeApiConfig, SpreeApiProvider } from '..'
|
||||
import type { IPage } from '@spree/storefront-api-v2-sdk/types/interfaces/Page'
|
||||
import normalizePage from '../../utils/normalizations/normalize-page'
|
||||
|
||||
export type Page = any
|
||||
export type GetPageResult = { page?: Page }
|
||||
|
||||
export type PageVariables = {
|
||||
id: number
|
||||
}
|
||||
|
||||
export default function getPageOperation({
|
||||
commerce,
|
||||
}: OperationContext<SpreeApiProvider>) {
|
||||
async function getPage<T extends GetPageOperation>(opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
}): Promise<T['data']>
|
||||
|
||||
async function getPage<T extends GetPageOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
async function getPage<T extends GetPageOperation>({
|
||||
url,
|
||||
config: userConfig,
|
||||
preview,
|
||||
variables: getPageVariables,
|
||||
}: {
|
||||
url?: string
|
||||
variables: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
}): Promise<T['data']> {
|
||||
console.info(
|
||||
'getPage called. Configuration: ',
|
||||
'userConfig: ',
|
||||
userConfig,
|
||||
'preview: ',
|
||||
preview,
|
||||
'url: ',
|
||||
url
|
||||
)
|
||||
|
||||
const config = commerce.getConfig(userConfig)
|
||||
const { fetch: apiFetch } = config
|
||||
|
||||
const variables: SpreeSdkVariables = {
|
||||
methodPath: 'pages.show',
|
||||
arguments: [getPageVariables.id],
|
||||
}
|
||||
|
||||
const { data: spreeSuccessResponse } = await apiFetch<
|
||||
IPage,
|
||||
SpreeSdkVariables
|
||||
>('__UNUSED__', {
|
||||
variables,
|
||||
})
|
||||
|
||||
const normalizedPage: Page = normalizePage(
|
||||
spreeSuccessResponse,
|
||||
spreeSuccessResponse.data,
|
||||
config.locales || []
|
||||
)
|
||||
|
||||
return { page: normalizedPage }
|
||||
}
|
||||
|
||||
return getPage
|
||||
}
|
90
packages/spree/src/api/operations/get-product.ts
Normal file
90
packages/spree/src/api/operations/get-product.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { SpreeApiConfig, SpreeApiProvider } from '../index'
|
||||
import type { GetProductOperation } from '@vercel/commerce/types/product'
|
||||
import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type { IProduct } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
||||
import type { SpreeSdkVariables } from '../../types'
|
||||
import MissingSlugVariableError from '../../errors/MissingSlugVariableError'
|
||||
import normalizeProduct from '../../utils/normalizations/normalize-product'
|
||||
import { requireConfigValue } from '../../isomorphic-config'
|
||||
|
||||
const imagesSize = requireConfigValue('imagesSize') as string
|
||||
const imagesQuality = requireConfigValue('imagesQuality') as number
|
||||
|
||||
export default function getProductOperation({
|
||||
commerce,
|
||||
}: OperationContext<SpreeApiProvider>) {
|
||||
async function getProduct<T extends GetProductOperation>(opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
}): Promise<T['data']>
|
||||
|
||||
async function getProduct<T extends GetProductOperation>(
|
||||
opts: {
|
||||
variables: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
async function getProduct<T extends GetProductOperation>({
|
||||
query = '',
|
||||
variables: getProductVariables,
|
||||
config: userConfig,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: T['variables']
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
}): Promise<T['data']> {
|
||||
console.log(
|
||||
'getProduct called. Configuration: ',
|
||||
'getProductVariables: ',
|
||||
getProductVariables,
|
||||
'config: ',
|
||||
userConfig
|
||||
)
|
||||
|
||||
if (!getProductVariables?.slug) {
|
||||
throw new MissingSlugVariableError()
|
||||
}
|
||||
|
||||
const variables: SpreeSdkVariables = {
|
||||
methodPath: 'products.show',
|
||||
arguments: [
|
||||
getProductVariables.slug,
|
||||
{},
|
||||
{
|
||||
include:
|
||||
'primary_variant,variants,images,option_types,variants.option_values',
|
||||
image_transformation: {
|
||||
quality: imagesQuality,
|
||||
size: imagesSize,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const config = commerce.getConfig(userConfig)
|
||||
const { fetch: apiFetch } = config // TODO: Send config.locale to Spree.
|
||||
|
||||
const { data: spreeSuccessResponse } = await apiFetch<
|
||||
IProduct,
|
||||
SpreeSdkVariables
|
||||
>('__UNUSED__', {
|
||||
variables,
|
||||
})
|
||||
|
||||
return {
|
||||
product: normalizeProduct(
|
||||
spreeSuccessResponse,
|
||||
spreeSuccessResponse.data
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
return getProduct
|
||||
}
|
138
packages/spree/src/api/operations/get-site-info.ts
Normal file
138
packages/spree/src/api/operations/get-site-info.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import type {
|
||||
OperationContext,
|
||||
OperationOptions,
|
||||
} from '@vercel/commerce/api/operations'
|
||||
import type {
|
||||
Category,
|
||||
GetSiteInfoOperation,
|
||||
} from '@vercel/commerce/types/site'
|
||||
import type {
|
||||
ITaxons,
|
||||
TaxonAttr,
|
||||
} from '@spree/storefront-api-v2-sdk/types/interfaces/Taxon'
|
||||
import { requireConfigValue } from '../../isomorphic-config'
|
||||
import type { SpreeSdkVariables } from '../../types'
|
||||
import type { SpreeApiConfig, SpreeApiProvider } from '..'
|
||||
|
||||
const taxonsSort = (spreeTaxon1: TaxonAttr, spreeTaxon2: TaxonAttr): number => {
|
||||
const { left: left1, right: right1 } = spreeTaxon1.attributes
|
||||
const { left: left2, right: right2 } = spreeTaxon2.attributes
|
||||
|
||||
if (right1 < left2) {
|
||||
return -1
|
||||
}
|
||||
|
||||
if (right2 < left1) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
export type GetSiteInfoResult<
|
||||
T extends { categories: any[]; brands: any[] } = {
|
||||
categories: Category[]
|
||||
brands: any[]
|
||||
}
|
||||
> = T
|
||||
|
||||
export default function getSiteInfoOperation({
|
||||
commerce,
|
||||
}: OperationContext<SpreeApiProvider>) {
|
||||
async function getSiteInfo<T extends GetSiteInfoOperation>(opts?: {
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
}): Promise<T['data']>
|
||||
|
||||
async function getSiteInfo<T extends GetSiteInfoOperation>(
|
||||
opts: {
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
} & OperationOptions
|
||||
): Promise<T['data']>
|
||||
|
||||
async function getSiteInfo<T extends GetSiteInfoOperation>({
|
||||
query,
|
||||
variables: getSiteInfoVariables = {},
|
||||
config: userConfig,
|
||||
}: {
|
||||
query?: string
|
||||
variables?: any
|
||||
config?: Partial<SpreeApiConfig>
|
||||
preview?: boolean
|
||||
} = {}): Promise<GetSiteInfoResult> {
|
||||
console.info(
|
||||
'getSiteInfo called. Configuration: ',
|
||||
'query: ',
|
||||
query,
|
||||
'getSiteInfoVariables ',
|
||||
getSiteInfoVariables,
|
||||
'config: ',
|
||||
userConfig
|
||||
)
|
||||
|
||||
const createVariables = (parentPermalink: string): SpreeSdkVariables => ({
|
||||
methodPath: 'taxons.list',
|
||||
arguments: [
|
||||
{
|
||||
filter: {
|
||||
parent_permalink: parentPermalink,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const config = commerce.getConfig(userConfig)
|
||||
const { fetch: apiFetch } = config // TODO: Send config.locale to Spree.
|
||||
|
||||
const { data: spreeCategoriesSuccessResponse } = await apiFetch<
|
||||
ITaxons,
|
||||
SpreeSdkVariables
|
||||
>('__UNUSED__', {
|
||||
variables: createVariables(
|
||||
requireConfigValue('categoriesTaxonomyPermalink') as string
|
||||
),
|
||||
})
|
||||
|
||||
const { data: spreeBrandsSuccessResponse } = await apiFetch<
|
||||
ITaxons,
|
||||
SpreeSdkVariables
|
||||
>('__UNUSED__', {
|
||||
variables: createVariables(
|
||||
requireConfigValue('brandsTaxonomyPermalink') as string
|
||||
),
|
||||
})
|
||||
|
||||
const normalizedCategories: GetSiteInfoOperation['data']['categories'] =
|
||||
spreeCategoriesSuccessResponse.data
|
||||
.sort(taxonsSort)
|
||||
.map((spreeTaxon: TaxonAttr) => {
|
||||
return {
|
||||
id: spreeTaxon.id,
|
||||
name: spreeTaxon.attributes.name,
|
||||
slug: spreeTaxon.id,
|
||||
path: spreeTaxon.id,
|
||||
}
|
||||
})
|
||||
|
||||
const normalizedBrands: GetSiteInfoOperation['data']['brands'] =
|
||||
spreeBrandsSuccessResponse.data
|
||||
.sort(taxonsSort)
|
||||
.map((spreeTaxon: TaxonAttr) => {
|
||||
return {
|
||||
node: {
|
||||
entityId: spreeTaxon.id,
|
||||
path: `brands/${spreeTaxon.id}`,
|
||||
name: spreeTaxon.attributes.name,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
categories: normalizedCategories,
|
||||
brands: normalizedBrands,
|
||||
}
|
||||
}
|
||||
|
||||
return getSiteInfo
|
||||
}
|
6
packages/spree/src/api/operations/index.ts
Normal file
6
packages/spree/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'
|
86
packages/spree/src/api/utils/create-api-fetch.ts
Normal file
86
packages/spree/src/api/utils/create-api-fetch.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { SpreeApiConfig } from '..'
|
||||
import { errors, makeClient } from '@spree/storefront-api-v2-sdk'
|
||||
import { requireConfigValue } from '../../isomorphic-config'
|
||||
import convertSpreeErrorToGraphQlError from '../../utils/convert-spree-error-to-graph-ql-error'
|
||||
import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse'
|
||||
import getSpreeSdkMethodFromEndpointPath from '../../utils/get-spree-sdk-method-from-endpoint-path'
|
||||
import SpreeSdkMethodFromEndpointPathError from '../../errors/SpreeSdkMethodFromEndpointPathError'
|
||||
import { GraphQLFetcher, GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import createCustomizedFetchFetcher, {
|
||||
fetchResponseKey,
|
||||
} from '../../utils/create-customized-fetch-fetcher'
|
||||
import fetch, { Request } from 'node-fetch'
|
||||
import type { SpreeSdkResponseWithRawResponse } from '../../types'
|
||||
import prettyPrintSpreeSdkErrors from '../../utils/pretty-print-spree-sdk-errors'
|
||||
|
||||
export type CreateApiFetch = (
|
||||
getConfig: () => SpreeApiConfig
|
||||
) => GraphQLFetcher<GraphQLFetcherResult<any>, any>
|
||||
|
||||
// TODO: GraphQLFetcher<GraphQLFetcherResult<any>, any> should be GraphQLFetcher<GraphQLFetcherResult<any>, SpreeSdkVariables>.
|
||||
// But CommerceAPIConfig['fetch'] cannot be extended from Variables = any to SpreeSdkVariables.
|
||||
|
||||
const createApiFetch: CreateApiFetch = (_getConfig) => {
|
||||
const client = makeClient({
|
||||
host: requireConfigValue('apiHost') as string,
|
||||
createFetcher: (fetcherOptions) => {
|
||||
return createCustomizedFetchFetcher({
|
||||
fetch,
|
||||
requestConstructor: Request,
|
||||
...fetcherOptions,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
return async (url, queryData = {}, fetchOptions = {}) => {
|
||||
console.log(
|
||||
'apiFetch called. query = ',
|
||||
'url = ',
|
||||
url,
|
||||
'queryData = ',
|
||||
queryData,
|
||||
'fetchOptions = ',
|
||||
fetchOptions
|
||||
)
|
||||
|
||||
const { variables } = queryData
|
||||
|
||||
if (!variables) {
|
||||
throw new SpreeSdkMethodFromEndpointPathError(
|
||||
`Required SpreeSdkVariables not provided.`
|
||||
)
|
||||
}
|
||||
|
||||
const storeResponse: ResultResponse<SpreeSdkResponseWithRawResponse> =
|
||||
await getSpreeSdkMethodFromEndpointPath(
|
||||
client,
|
||||
variables.methodPath
|
||||
)(...variables.arguments)
|
||||
|
||||
if (storeResponse.isSuccess()) {
|
||||
const data = storeResponse.success()
|
||||
const rawFetchResponse = data[fetchResponseKey]
|
||||
|
||||
return {
|
||||
data,
|
||||
res: rawFetchResponse,
|
||||
}
|
||||
}
|
||||
|
||||
const storeResponseError = storeResponse.fail()
|
||||
|
||||
if (storeResponseError instanceof errors.SpreeError) {
|
||||
console.error(
|
||||
`Request to spree resulted in an error:\n\n${prettyPrintSpreeSdkErrors(
|
||||
storeResponse.fail()
|
||||
)}`
|
||||
)
|
||||
|
||||
throw convertSpreeErrorToGraphQlError(storeResponseError)
|
||||
}
|
||||
|
||||
throw storeResponseError
|
||||
}
|
||||
}
|
||||
|
||||
export default createApiFetch
|
3
packages/spree/src/api/utils/fetch.ts
Normal file
3
packages/spree/src/api/utils/fetch.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import vercelFetch from '@vercel/fetch'
|
||||
|
||||
export default vercelFetch()
|
3
packages/spree/src/auth/index.ts
Normal file
3
packages/spree/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'
|
86
packages/spree/src/auth/use-login.tsx
Normal file
86
packages/spree/src/auth/use-login.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useLogin, { UseLogin } from '@vercel/commerce/auth/use-login'
|
||||
import type { LoginHook } from '@vercel/commerce/types/login'
|
||||
import type { AuthTokenAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Authentication'
|
||||
import { FetcherError, ValidationError } from '@vercel/commerce/utils/errors'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useCart from '../cart/use-cart'
|
||||
import useWishlist from '../wishlist/use-wishlist'
|
||||
import login from '../utils/login'
|
||||
|
||||
export default useLogin as UseLogin<typeof handler>
|
||||
|
||||
export const handler: MutationHook<LoginHook> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
url: 'authentication',
|
||||
query: 'getToken',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
console.info(
|
||||
'useLogin fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
const { email, password } = input
|
||||
|
||||
if (!email || !password) {
|
||||
throw new ValidationError({
|
||||
message: 'Email and password need to be provided.',
|
||||
})
|
||||
}
|
||||
|
||||
const getTokenParameters: AuthTokenAttr = {
|
||||
username: email,
|
||||
password,
|
||||
}
|
||||
|
||||
try {
|
||||
await login(fetch, getTokenParameters, false)
|
||||
|
||||
return null
|
||||
} catch (getTokenError) {
|
||||
if (
|
||||
getTokenError instanceof FetcherError &&
|
||||
getTokenError.status === 400
|
||||
) {
|
||||
// Change the error message to be more user friendly.
|
||||
throw new FetcherError({
|
||||
status: getTokenError.status,
|
||||
message: 'The email or password is invalid.',
|
||||
code: getTokenError.code,
|
||||
})
|
||||
}
|
||||
|
||||
throw getTokenError
|
||||
}
|
||||
},
|
||||
useHook: ({ fetch }) => {
|
||||
const useWrappedHook: ReturnType<
|
||||
MutationHook<LoginHook>['useHook']
|
||||
> = () => {
|
||||
const customer = useCustomer()
|
||||
const cart = useCart()
|
||||
const wishlist = useWishlist()
|
||||
|
||||
return useCallback(
|
||||
async function login(input) {
|
||||
const data = await fetch({ input })
|
||||
|
||||
await customer.mutate()
|
||||
await cart.mutate()
|
||||
await wishlist.mutate()
|
||||
|
||||
return data
|
||||
},
|
||||
[customer, cart, wishlist]
|
||||
)
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
81
packages/spree/src/auth/use-logout.tsx
Normal file
81
packages/spree/src/auth/use-logout.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useLogout, { UseLogout } from '@vercel/commerce/auth/use-logout'
|
||||
import type { LogoutHook } from '@vercel/commerce/types/logout'
|
||||
import { useCallback } from 'react'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useCart from '../cart/use-cart'
|
||||
import useWishlist from '../wishlist/use-wishlist'
|
||||
import {
|
||||
ensureUserTokenResponse,
|
||||
removeUserTokenResponse,
|
||||
} from '../utils/tokens/user-token-response'
|
||||
import revokeUserTokens from '../utils/tokens/revoke-user-tokens'
|
||||
import TokensNotRejectedError from '../errors/TokensNotRejectedError'
|
||||
|
||||
export default useLogout as UseLogout<typeof handler>
|
||||
|
||||
export const handler: MutationHook<LogoutHook> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
url: 'authentication',
|
||||
query: 'revokeToken',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
console.info(
|
||||
'useLogout fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
const userToken = ensureUserTokenResponse()
|
||||
|
||||
if (userToken) {
|
||||
try {
|
||||
// Revoke any tokens associated with the logged in user.
|
||||
await revokeUserTokens(fetch, {
|
||||
accessToken: userToken.access_token,
|
||||
refreshToken: userToken.refresh_token,
|
||||
})
|
||||
} catch (revokeUserTokenError) {
|
||||
// Squash token revocation errors and rethrow anything else.
|
||||
if (!(revokeUserTokenError instanceof TokensNotRejectedError)) {
|
||||
throw revokeUserTokenError
|
||||
}
|
||||
}
|
||||
|
||||
// Whether token revocation succeeded or not, remove them from local storage.
|
||||
removeUserTokenResponse()
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
useHook: ({ fetch }) => {
|
||||
const useWrappedHook: ReturnType<
|
||||
MutationHook<LogoutHook>['useHook']
|
||||
> = () => {
|
||||
const customer = useCustomer({
|
||||
swrOptions: { isPaused: () => true },
|
||||
})
|
||||
const cart = useCart({
|
||||
swrOptions: { isPaused: () => true },
|
||||
})
|
||||
const wishlist = useWishlist({
|
||||
swrOptions: { isPaused: () => true },
|
||||
})
|
||||
|
||||
return useCallback(async () => {
|
||||
const data = await fetch()
|
||||
|
||||
await customer.mutate(null, false)
|
||||
await cart.mutate(null, false)
|
||||
await wishlist.mutate(null, false)
|
||||
|
||||
return data
|
||||
}, [customer, cart, wishlist])
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
96
packages/spree/src/auth/use-signup.tsx
Normal file
96
packages/spree/src/auth/use-signup.tsx
Normal file
@@ -0,0 +1,96 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useSignup, { UseSignup } from '@vercel/commerce/auth/use-signup'
|
||||
import type { SignupHook } from '@vercel/commerce/types/signup'
|
||||
import { ValidationError } from '@vercel/commerce/utils/errors'
|
||||
import type { IAccount } from '@spree/storefront-api-v2-sdk/types/interfaces/Account'
|
||||
import type { AuthTokenAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Authentication'
|
||||
import useCustomer from '../customer/use-customer'
|
||||
import useCart from '../cart/use-cart'
|
||||
import useWishlist from '../wishlist/use-wishlist'
|
||||
import login from '../utils/login'
|
||||
import { requireConfigValue } from '../isomorphic-config'
|
||||
|
||||
export default useSignup as UseSignup<typeof handler>
|
||||
|
||||
export const handler: MutationHook<SignupHook> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
url: 'account',
|
||||
query: 'create',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
console.info(
|
||||
'useSignup fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
const { email, password } = input
|
||||
|
||||
if (!email || !password) {
|
||||
throw new ValidationError({
|
||||
message: 'Email and password need to be provided.',
|
||||
})
|
||||
}
|
||||
|
||||
// TODO: Replace any with specific type from Spree SDK
|
||||
// once it's added to the SDK.
|
||||
const createAccountParameters: any = {
|
||||
user: {
|
||||
email,
|
||||
password,
|
||||
// The stock NJC interface doesn't have a
|
||||
// password confirmation field, so just copy password.
|
||||
passwordConfirmation: password,
|
||||
},
|
||||
}
|
||||
|
||||
// Create the user account.
|
||||
await fetch<GraphQLFetcherResult<IAccount>>({
|
||||
variables: {
|
||||
methodPath: 'account.create',
|
||||
arguments: [createAccountParameters],
|
||||
},
|
||||
})
|
||||
|
||||
const getTokenParameters: AuthTokenAttr = {
|
||||
username: email,
|
||||
password,
|
||||
}
|
||||
|
||||
// Login immediately after the account is created.
|
||||
if (requireConfigValue('loginAfterSignup')) {
|
||||
await login(fetch, getTokenParameters, true)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
useHook: ({ fetch }) => {
|
||||
const useWrappedHook: ReturnType<
|
||||
MutationHook<SignupHook>['useHook']
|
||||
> = () => {
|
||||
const customer = useCustomer()
|
||||
const cart = useCart()
|
||||
const wishlist = useWishlist()
|
||||
|
||||
return useCallback(
|
||||
async (input) => {
|
||||
const data = await fetch({ input })
|
||||
|
||||
await customer.mutate()
|
||||
await cart.mutate()
|
||||
await wishlist.mutate()
|
||||
|
||||
return data
|
||||
},
|
||||
[customer, cart, wishlist]
|
||||
)
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
4
packages/spree/src/cart/index.ts
Normal file
4
packages/spree/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'
|
118
packages/spree/src/cart/use-add-item.tsx
Normal file
118
packages/spree/src/cart/use-add-item.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import useAddItem from '@vercel/commerce/cart/use-add-item'
|
||||
import type { UseAddItem } from '@vercel/commerce/cart/use-add-item'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import { useCallback } from 'react'
|
||||
import useCart from './use-cart'
|
||||
import type { AddItemHook } from '@vercel/commerce/types/cart'
|
||||
import normalizeCart from '../utils/normalizations/normalize-cart'
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import type { IOrder } from '@spree/storefront-api-v2-sdk/types/interfaces/Order'
|
||||
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import type { AddItem } from '@spree/storefront-api-v2-sdk/types/interfaces/endpoints/CartClass'
|
||||
import { setCartToken } from '../utils/tokens/cart-token'
|
||||
import ensureIToken from '../utils/tokens/ensure-itoken'
|
||||
import createEmptyCart from '../utils/create-empty-cart'
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
import isLoggedIn from '../utils/tokens/is-logged-in'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<AddItemHook> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
url: 'cart',
|
||||
query: 'addItem',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
console.info(
|
||||
'useAddItem fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
const { quantity, productId, variantId } = input
|
||||
|
||||
const safeQuantity = quantity ?? 1
|
||||
|
||||
let token: IToken | undefined = ensureIToken()
|
||||
|
||||
const addItemParameters: AddItem = {
|
||||
variant_id: variantId,
|
||||
quantity: safeQuantity,
|
||||
include: [
|
||||
'line_items',
|
||||
'line_items.variant',
|
||||
'line_items.variant.product',
|
||||
'line_items.variant.product.images',
|
||||
'line_items.variant.images',
|
||||
'line_items.variant.option_values',
|
||||
'line_items.variant.product.option_types',
|
||||
].join(','),
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
const { data: spreeCartCreateSuccessResponse } = await createEmptyCart(
|
||||
fetch
|
||||
)
|
||||
|
||||
setCartToken(spreeCartCreateSuccessResponse.data.attributes.token)
|
||||
token = ensureIToken()
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: spreeSuccessResponse } = await fetch<
|
||||
GraphQLFetcherResult<IOrder>
|
||||
>({
|
||||
variables: {
|
||||
methodPath: 'cart.addItem',
|
||||
arguments: [token, addItemParameters],
|
||||
},
|
||||
})
|
||||
|
||||
return normalizeCart(spreeSuccessResponse, spreeSuccessResponse.data)
|
||||
} catch (addItemError) {
|
||||
if (addItemError instanceof FetcherError && addItemError.status === 404) {
|
||||
const { data: spreeRetroactiveCartCreateSuccessResponse } =
|
||||
await createEmptyCart(fetch)
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
setCartToken(
|
||||
spreeRetroactiveCartCreateSuccessResponse.data.attributes.token
|
||||
)
|
||||
}
|
||||
|
||||
// Return an empty cart. The user has to add the item again.
|
||||
// This is going to be a rare situation.
|
||||
|
||||
return normalizeCart(
|
||||
spreeRetroactiveCartCreateSuccessResponse,
|
||||
spreeRetroactiveCartCreateSuccessResponse.data
|
||||
)
|
||||
}
|
||||
|
||||
throw addItemError
|
||||
}
|
||||
},
|
||||
useHook: ({ fetch }) => {
|
||||
const useWrappedHook: ReturnType<
|
||||
MutationHook<AddItemHook>['useHook']
|
||||
> = () => {
|
||||
const { mutate } = useCart()
|
||||
|
||||
return useCallback(
|
||||
async (input) => {
|
||||
const data = await fetch({ input })
|
||||
|
||||
await mutate(data, false)
|
||||
|
||||
return data
|
||||
},
|
||||
[mutate]
|
||||
)
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
123
packages/spree/src/cart/use-cart.tsx
Normal file
123
packages/spree/src/cart/use-cart.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCart from '@vercel/commerce/cart/use-cart'
|
||||
import type { UseCart } from '@vercel/commerce/cart/use-cart'
|
||||
import type { GetCartHook } from '@vercel/commerce/types/cart'
|
||||
import normalizeCart from '../utils/normalizations/normalize-cart'
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import type { IOrder } from '@spree/storefront-api-v2-sdk/types/interfaces/Order'
|
||||
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
import { setCartToken } from '../utils/tokens/cart-token'
|
||||
import ensureIToken from '../utils/tokens/ensure-itoken'
|
||||
import isLoggedIn from '../utils/tokens/is-logged-in'
|
||||
import createEmptyCart from '../utils/create-empty-cart'
|
||||
import { requireConfigValue } from '../isomorphic-config'
|
||||
|
||||
const imagesSize = requireConfigValue('imagesSize') as string
|
||||
const imagesQuality = requireConfigValue('imagesQuality') as number
|
||||
|
||||
export default useCart as UseCart<typeof handler>
|
||||
|
||||
// This handler avoids calling /api/cart.
|
||||
// There doesn't seem to be a good reason to call it.
|
||||
// So far, only bigcommerce uses it.
|
||||
export const handler: SWRHook<GetCartHook> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
url: 'cart',
|
||||
query: 'show',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
console.info(
|
||||
'useCart fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
let spreeCartResponse: IOrder | null
|
||||
|
||||
const token: IToken | undefined = ensureIToken()
|
||||
|
||||
if (!token) {
|
||||
spreeCartResponse = null
|
||||
} else {
|
||||
try {
|
||||
const { data: spreeCartShowSuccessResponse } = await fetch<
|
||||
GraphQLFetcherResult<IOrder>
|
||||
>({
|
||||
variables: {
|
||||
methodPath: 'cart.show',
|
||||
arguments: [
|
||||
token,
|
||||
{
|
||||
include: [
|
||||
'line_items',
|
||||
'line_items.variant',
|
||||
'line_items.variant.product',
|
||||
'line_items.variant.product.images',
|
||||
'line_items.variant.images',
|
||||
'line_items.variant.option_values',
|
||||
'line_items.variant.product.option_types',
|
||||
].join(','),
|
||||
image_transformation: {
|
||||
quality: imagesQuality,
|
||||
size: imagesSize,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
spreeCartResponse = spreeCartShowSuccessResponse
|
||||
} catch (fetchCartError) {
|
||||
if (
|
||||
!(fetchCartError instanceof FetcherError) ||
|
||||
fetchCartError.status !== 404
|
||||
) {
|
||||
throw fetchCartError
|
||||
}
|
||||
|
||||
spreeCartResponse = null
|
||||
}
|
||||
}
|
||||
|
||||
if (!spreeCartResponse || spreeCartResponse?.data.attributes.completed_at) {
|
||||
const { data: spreeCartCreateSuccessResponse } = await createEmptyCart(
|
||||
fetch
|
||||
)
|
||||
|
||||
spreeCartResponse = spreeCartCreateSuccessResponse
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
setCartToken(spreeCartResponse.data.attributes.token)
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeCart(spreeCartResponse, spreeCartResponse.data)
|
||||
},
|
||||
useHook: ({ useData }) => {
|
||||
const useWrappedHook: ReturnType<SWRHook<GetCartHook>['useHook']> = (
|
||||
input
|
||||
) => {
|
||||
const response = useData({
|
||||
swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
|
||||
})
|
||||
|
||||
return useMemo<typeof response & { isEmpty: boolean }>(() => {
|
||||
return Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.lineItems.length ?? 0) === 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
})
|
||||
}, [response])
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
119
packages/spree/src/cart/use-remove-item.tsx
Normal file
119
packages/spree/src/cart/use-remove-item.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useRemoveItem from '@vercel/commerce/cart/use-remove-item'
|
||||
import type { UseRemoveItem } from '@vercel/commerce/cart/use-remove-item'
|
||||
import type { RemoveItemHook } from '@vercel/commerce/types/cart'
|
||||
import useCart from './use-cart'
|
||||
import { useCallback } from 'react'
|
||||
import normalizeCart from '../utils/normalizations/normalize-cart'
|
||||
import type { IOrder } from '@spree/storefront-api-v2-sdk/types/interfaces/Order'
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import type { IQuery } from '@spree/storefront-api-v2-sdk/types/interfaces/Query'
|
||||
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import ensureIToken from '../utils/tokens/ensure-itoken'
|
||||
import createEmptyCart from '../utils/create-empty-cart'
|
||||
import { setCartToken } from '../utils/tokens/cart-token'
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
import isLoggedIn from '../utils/tokens/is-logged-in'
|
||||
|
||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<RemoveItemHook> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
url: 'cart',
|
||||
query: 'removeItem',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
console.info(
|
||||
'useRemoveItem fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
const { itemId: lineItemId } = input
|
||||
|
||||
let token: IToken | undefined = ensureIToken()
|
||||
|
||||
if (!token) {
|
||||
const { data: spreeCartCreateSuccessResponse } = await createEmptyCart(
|
||||
fetch
|
||||
)
|
||||
|
||||
setCartToken(spreeCartCreateSuccessResponse.data.attributes.token)
|
||||
token = ensureIToken()
|
||||
}
|
||||
|
||||
const removeItemParameters: IQuery = {
|
||||
include: [
|
||||
'line_items',
|
||||
'line_items.variant',
|
||||
'line_items.variant.product',
|
||||
'line_items.variant.product.images',
|
||||
'line_items.variant.images',
|
||||
'line_items.variant.option_values',
|
||||
'line_items.variant.product.option_types',
|
||||
].join(','),
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: spreeSuccessResponse } = await fetch<
|
||||
GraphQLFetcherResult<IOrder>
|
||||
>({
|
||||
variables: {
|
||||
methodPath: 'cart.removeItem',
|
||||
arguments: [token, lineItemId, removeItemParameters],
|
||||
},
|
||||
})
|
||||
|
||||
return normalizeCart(spreeSuccessResponse, spreeSuccessResponse.data)
|
||||
} catch (removeItemError) {
|
||||
if (
|
||||
removeItemError instanceof FetcherError &&
|
||||
removeItemError.status === 404
|
||||
) {
|
||||
const { data: spreeRetroactiveCartCreateSuccessResponse } =
|
||||
await createEmptyCart(fetch)
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
setCartToken(
|
||||
spreeRetroactiveCartCreateSuccessResponse.data.attributes.token
|
||||
)
|
||||
}
|
||||
|
||||
// Return an empty cart. This is going to be a rare situation.
|
||||
|
||||
return normalizeCart(
|
||||
spreeRetroactiveCartCreateSuccessResponse,
|
||||
spreeRetroactiveCartCreateSuccessResponse.data
|
||||
)
|
||||
}
|
||||
|
||||
throw removeItemError
|
||||
}
|
||||
},
|
||||
useHook: ({ fetch }) => {
|
||||
const useWrappedHook: ReturnType<
|
||||
MutationHook<RemoveItemHook>['useHook']
|
||||
> = () => {
|
||||
const { mutate } = useCart()
|
||||
|
||||
return useCallback(
|
||||
async (input) => {
|
||||
const data = await fetch({ input: { itemId: input.id } })
|
||||
|
||||
// Upon calling cart.removeItem, Spree returns the old version of the cart,
|
||||
// with the already removed line item. Invalidate the useCart mutation
|
||||
// to fetch the cart again.
|
||||
await mutate(data, true)
|
||||
|
||||
return data
|
||||
},
|
||||
[mutate]
|
||||
)
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
148
packages/spree/src/cart/use-update-item.tsx
Normal file
148
packages/spree/src/cart/use-update-item.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useUpdateItem, {
|
||||
UseUpdateItem,
|
||||
} from '@vercel/commerce/cart/use-update-item'
|
||||
import type { UpdateItemHook } from '@vercel/commerce/types/cart'
|
||||
import useCart from './use-cart'
|
||||
import { useMemo } from 'react'
|
||||
import { FetcherError, ValidationError } from '@vercel/commerce/utils/errors'
|
||||
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import type { SetQuantity } from '@spree/storefront-api-v2-sdk/types/interfaces/endpoints/CartClass'
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import type { IOrder } from '@spree/storefront-api-v2-sdk/types/interfaces/Order'
|
||||
import normalizeCart from '../utils/normalizations/normalize-cart'
|
||||
import debounce from 'lodash.debounce'
|
||||
import ensureIToken from '../utils/tokens/ensure-itoken'
|
||||
import createEmptyCart from '../utils/create-empty-cart'
|
||||
import { setCartToken } from '../utils/tokens/cart-token'
|
||||
import isLoggedIn from '../utils/tokens/is-logged-in'
|
||||
|
||||
export default useUpdateItem as UseUpdateItem<any>
|
||||
|
||||
export const handler: MutationHook<UpdateItemHook> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
url: 'cart',
|
||||
query: 'setQuantity',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
console.info(
|
||||
'useRemoveItem fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
const { itemId, item } = input
|
||||
|
||||
if (!item.quantity) {
|
||||
throw new ValidationError({
|
||||
message: 'Line item quantity needs to be provided.',
|
||||
})
|
||||
}
|
||||
|
||||
let token: IToken | undefined = ensureIToken()
|
||||
|
||||
if (!token) {
|
||||
const { data: spreeCartCreateSuccessResponse } = await createEmptyCart(
|
||||
fetch
|
||||
)
|
||||
|
||||
setCartToken(spreeCartCreateSuccessResponse.data.attributes.token)
|
||||
token = ensureIToken()
|
||||
}
|
||||
|
||||
try {
|
||||
const setQuantityParameters: SetQuantity = {
|
||||
line_item_id: itemId,
|
||||
quantity: item.quantity,
|
||||
include: [
|
||||
'line_items',
|
||||
'line_items.variant',
|
||||
'line_items.variant.product',
|
||||
'line_items.variant.product.images',
|
||||
'line_items.variant.images',
|
||||
'line_items.variant.option_values',
|
||||
'line_items.variant.product.option_types',
|
||||
].join(','),
|
||||
}
|
||||
|
||||
const { data: spreeSuccessResponse } = await fetch<
|
||||
GraphQLFetcherResult<IOrder>
|
||||
>({
|
||||
variables: {
|
||||
methodPath: 'cart.setQuantity',
|
||||
arguments: [token, setQuantityParameters],
|
||||
},
|
||||
})
|
||||
|
||||
return normalizeCart(spreeSuccessResponse, spreeSuccessResponse.data)
|
||||
} catch (updateItemError) {
|
||||
if (
|
||||
updateItemError instanceof FetcherError &&
|
||||
updateItemError.status === 404
|
||||
) {
|
||||
const { data: spreeRetroactiveCartCreateSuccessResponse } =
|
||||
await createEmptyCart(fetch)
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
setCartToken(
|
||||
spreeRetroactiveCartCreateSuccessResponse.data.attributes.token
|
||||
)
|
||||
}
|
||||
|
||||
// Return an empty cart. The user has to update the item again.
|
||||
// This is going to be a rare situation.
|
||||
|
||||
return normalizeCart(
|
||||
spreeRetroactiveCartCreateSuccessResponse,
|
||||
spreeRetroactiveCartCreateSuccessResponse.data
|
||||
)
|
||||
}
|
||||
|
||||
throw updateItemError
|
||||
}
|
||||
},
|
||||
useHook: ({ fetch }) => {
|
||||
const useWrappedHook: ReturnType<
|
||||
MutationHook<UpdateItemHook>['useHook']
|
||||
> = (context) => {
|
||||
const { mutate } = useCart()
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
debounce(async (input: UpdateItemHook['actionInput']) => {
|
||||
const itemId = context?.item?.id
|
||||
const productId = input.productId ?? context?.item?.productId
|
||||
const variantId = input.variantId ?? context?.item?.variantId
|
||||
const quantity = input.quantity
|
||||
|
||||
if (!itemId || !productId || !variantId) {
|
||||
throw new ValidationError({
|
||||
message: 'Invalid input used for this operation',
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fetch({
|
||||
input: {
|
||||
item: {
|
||||
productId,
|
||||
variantId,
|
||||
quantity,
|
||||
},
|
||||
itemId,
|
||||
},
|
||||
})
|
||||
|
||||
await mutate(data, false)
|
||||
|
||||
return data
|
||||
}, context?.wait ?? 500),
|
||||
[mutate, context]
|
||||
)
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
19
packages/spree/src/checkout/use-checkout.tsx
Normal file
19
packages/spree/src/checkout/use-checkout.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
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> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
// TODO: Revise url and query
|
||||
url: 'checkout',
|
||||
query: 'show',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {},
|
||||
useHook:
|
||||
({ useData }) =>
|
||||
async (input) => ({}),
|
||||
}
|
10
packages/spree/src/commerce.config.json
Normal file
10
packages/spree/src/commerce.config.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"provider": "spree",
|
||||
"features": {
|
||||
"wishlist": true,
|
||||
"cart": true,
|
||||
"search": true,
|
||||
"customerAuth": true,
|
||||
"customCheckout": false
|
||||
}
|
||||
}
|
18
packages/spree/src/customer/address/use-add-item.tsx
Normal file
18
packages/spree/src/customer/address/use-add-item.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import useAddItem from '@vercel/commerce/customer/address/use-add-item'
|
||||
import type { UseAddItem } from '@vercel/commerce/customer/address/use-add-item'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
url: 'account',
|
||||
query: 'createAddress',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {},
|
||||
useHook:
|
||||
({ fetch }) =>
|
||||
() =>
|
||||
async () => ({}),
|
||||
}
|
19
packages/spree/src/customer/card/use-add-item.tsx
Normal file
19
packages/spree/src/customer/card/use-add-item.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import useAddItem from '@vercel/commerce/customer/address/use-add-item'
|
||||
import type { UseAddItem } from '@vercel/commerce/customer/address/use-add-item'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<any> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
// TODO: Revise url and query
|
||||
url: 'checkout',
|
||||
query: 'addPayment',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {},
|
||||
useHook:
|
||||
({ fetch }) =>
|
||||
() =>
|
||||
async () => ({}),
|
||||
}
|
1
packages/spree/src/customer/index.ts
Normal file
1
packages/spree/src/customer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as useCustomer } from './use-customer'
|
83
packages/spree/src/customer/use-customer.tsx
Normal file
83
packages/spree/src/customer/use-customer.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useCustomer from '@vercel/commerce/customer/use-customer'
|
||||
import type { UseCustomer } from '@vercel/commerce/customer/use-customer'
|
||||
import type { CustomerHook } from '@vercel/commerce/types/customer'
|
||||
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import type { IAccount } from '@spree/storefront-api-v2-sdk/types/interfaces/Account'
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
import normalizeUser from '../utils/normalizations/normalize-user'
|
||||
import isLoggedIn from '../utils/tokens/is-logged-in'
|
||||
import ensureIToken from '../utils/tokens/ensure-itoken'
|
||||
|
||||
export default useCustomer as UseCustomer<typeof handler>
|
||||
|
||||
export const handler: SWRHook<CustomerHook> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
url: 'account',
|
||||
query: 'get',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
console.info(
|
||||
'useCustomer fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
return null
|
||||
}
|
||||
|
||||
const token: IToken | undefined = ensureIToken()
|
||||
|
||||
if (!token) {
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
const { data: spreeAccountInfoSuccessResponse } = await fetch<
|
||||
GraphQLFetcherResult<IAccount>
|
||||
>({
|
||||
variables: {
|
||||
methodPath: 'account.accountInfo',
|
||||
arguments: [token],
|
||||
},
|
||||
})
|
||||
|
||||
const spreeUser = spreeAccountInfoSuccessResponse.data
|
||||
|
||||
const normalizedUser = normalizeUser(
|
||||
spreeAccountInfoSuccessResponse,
|
||||
spreeUser
|
||||
)
|
||||
|
||||
return normalizedUser
|
||||
} catch (fetchUserError) {
|
||||
if (
|
||||
!(fetchUserError instanceof FetcherError) ||
|
||||
fetchUserError.status !== 404
|
||||
) {
|
||||
throw fetchUserError
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
},
|
||||
useHook: ({ useData }) => {
|
||||
const useWrappedHook: ReturnType<SWRHook<CustomerHook>['useHook']> = (
|
||||
input
|
||||
) => {
|
||||
return useData({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input?.swrOptions,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
1
packages/spree/src/errors/AccessTokenError.ts
Normal file
1
packages/spree/src/errors/AccessTokenError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class AccessTokenError extends Error {}
|
1
packages/spree/src/errors/MisconfigurationError.ts
Normal file
1
packages/spree/src/errors/MisconfigurationError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class MisconfigurationError extends Error {}
|
@@ -0,0 +1 @@
|
||||
export default class MissingConfigurationValueError extends Error {}
|
1
packages/spree/src/errors/MissingLineItemVariantError.ts
Normal file
1
packages/spree/src/errors/MissingLineItemVariantError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class MissingLineItemVariantError extends Error {}
|
1
packages/spree/src/errors/MissingOptionValueError.ts
Normal file
1
packages/spree/src/errors/MissingOptionValueError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class MissingOptionValueError extends Error {}
|
1
packages/spree/src/errors/MissingPrimaryVariantError.ts
Normal file
1
packages/spree/src/errors/MissingPrimaryVariantError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class MissingPrimaryVariantError extends Error {}
|
1
packages/spree/src/errors/MissingProductError.ts
Normal file
1
packages/spree/src/errors/MissingProductError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class MissingProductError extends Error {}
|
1
packages/spree/src/errors/MissingSlugVariableError.ts
Normal file
1
packages/spree/src/errors/MissingSlugVariableError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class MissingSlugVariableError extends Error {}
|
1
packages/spree/src/errors/MissingVariantError.ts
Normal file
1
packages/spree/src/errors/MissingVariantError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class MissingVariantError extends Error {}
|
1
packages/spree/src/errors/RefreshTokenError.ts
Normal file
1
packages/spree/src/errors/RefreshTokenError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class RefreshTokenError extends Error {}
|
1
packages/spree/src/errors/SpreeResponseContentError.ts
Normal file
1
packages/spree/src/errors/SpreeResponseContentError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class SpreeResponseContentError extends Error {}
|
@@ -0,0 +1 @@
|
||||
export default class SpreeSdkMethodFromEndpointPathError extends Error {}
|
1
packages/spree/src/errors/TokensNotRejectedError.ts
Normal file
1
packages/spree/src/errors/TokensNotRejectedError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class TokensNotRejectedError extends Error {}
|
1
packages/spree/src/errors/UserTokenResponseParseError.ts
Normal file
1
packages/spree/src/errors/UserTokenResponseParseError.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default class UserTokenResponseParseError extends Error {}
|
123
packages/spree/src/fetcher.ts
Normal file
123
packages/spree/src/fetcher.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import type { Fetcher } from '@vercel/commerce/utils/types'
|
||||
import convertSpreeErrorToGraphQlError from './utils/convert-spree-error-to-graph-ql-error'
|
||||
import { makeClient, errors } from '@spree/storefront-api-v2-sdk'
|
||||
import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse'
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import { requireConfigValue } from './isomorphic-config'
|
||||
import getSpreeSdkMethodFromEndpointPath from './utils/get-spree-sdk-method-from-endpoint-path'
|
||||
import SpreeSdkMethodFromEndpointPathError from './errors/SpreeSdkMethodFromEndpointPathError'
|
||||
import type {
|
||||
FetcherVariables,
|
||||
SpreeSdkResponse,
|
||||
SpreeSdkResponseWithRawResponse,
|
||||
} from './types'
|
||||
import createCustomizedFetchFetcher, {
|
||||
fetchResponseKey,
|
||||
} from './utils/create-customized-fetch-fetcher'
|
||||
import ensureFreshUserAccessToken from './utils/tokens/ensure-fresh-user-access-token'
|
||||
import RefreshTokenError from './errors/RefreshTokenError'
|
||||
import prettyPrintSpreeSdkErrors from './utils/pretty-print-spree-sdk-errors'
|
||||
|
||||
const client = makeClient({
|
||||
host: requireConfigValue('apiHost') as string,
|
||||
createFetcher: (fetcherOptions) => {
|
||||
return createCustomizedFetchFetcher({
|
||||
fetch: globalThis.fetch,
|
||||
requestConstructor: globalThis.Request,
|
||||
...fetcherOptions,
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const normalizeSpreeSuccessResponse = (
|
||||
storeResponse: ResultResponse<SpreeSdkResponseWithRawResponse>
|
||||
): GraphQLFetcherResult<SpreeSdkResponse> => {
|
||||
const data = storeResponse.success()
|
||||
const rawFetchResponse = data[fetchResponseKey]
|
||||
|
||||
return {
|
||||
data,
|
||||
res: rawFetchResponse,
|
||||
}
|
||||
}
|
||||
|
||||
const fetcher: Fetcher<GraphQLFetcherResult<SpreeSdkResponse>> = async (
|
||||
requestOptions
|
||||
) => {
|
||||
const { url, method, variables, query } = requestOptions
|
||||
|
||||
console.log(
|
||||
'Fetcher called. Configuration: ',
|
||||
'url = ',
|
||||
url,
|
||||
'requestOptions = ',
|
||||
requestOptions
|
||||
)
|
||||
|
||||
if (!variables) {
|
||||
throw new SpreeSdkMethodFromEndpointPathError(
|
||||
`Required FetcherVariables not provided.`
|
||||
)
|
||||
}
|
||||
|
||||
const {
|
||||
methodPath,
|
||||
arguments: args,
|
||||
refreshExpiredAccessToken = true,
|
||||
replayUnauthorizedRequest = true,
|
||||
} = variables as FetcherVariables
|
||||
|
||||
if (refreshExpiredAccessToken) {
|
||||
await ensureFreshUserAccessToken(client)
|
||||
}
|
||||
|
||||
const spreeSdkMethod = getSpreeSdkMethodFromEndpointPath(client, methodPath)
|
||||
|
||||
const storeResponse: ResultResponse<SpreeSdkResponseWithRawResponse> =
|
||||
await spreeSdkMethod(...args)
|
||||
|
||||
if (storeResponse.isSuccess()) {
|
||||
return normalizeSpreeSuccessResponse(storeResponse)
|
||||
}
|
||||
|
||||
const storeResponseError = storeResponse.fail()
|
||||
|
||||
if (
|
||||
storeResponseError instanceof errors.SpreeError &&
|
||||
storeResponseError.serverResponse.status === 401 &&
|
||||
replayUnauthorizedRequest
|
||||
) {
|
||||
console.info(
|
||||
'Request ended with 401. Replaying request after refreshing the user token.'
|
||||
)
|
||||
|
||||
await ensureFreshUserAccessToken(client)
|
||||
|
||||
const replayedStoreResponse: ResultResponse<SpreeSdkResponseWithRawResponse> =
|
||||
await spreeSdkMethod(...args)
|
||||
|
||||
if (replayedStoreResponse.isSuccess()) {
|
||||
return normalizeSpreeSuccessResponse(replayedStoreResponse)
|
||||
}
|
||||
|
||||
console.warn('Replaying the request failed', replayedStoreResponse.fail())
|
||||
|
||||
throw new RefreshTokenError(
|
||||
'Could not authorize request with current access token.'
|
||||
)
|
||||
}
|
||||
|
||||
if (storeResponseError instanceof errors.SpreeError) {
|
||||
console.error(
|
||||
`Request to spree resulted in an error:\n\n${prettyPrintSpreeSdkErrors(
|
||||
storeResponse.fail()
|
||||
)}`
|
||||
)
|
||||
|
||||
throw convertSpreeErrorToGraphQlError(storeResponseError)
|
||||
}
|
||||
|
||||
throw storeResponseError
|
||||
}
|
||||
|
||||
export default fetcher
|
49
packages/spree/src/index.tsx
Normal file
49
packages/spree/src/index.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { ComponentType, FunctionComponent } from 'react'
|
||||
import {
|
||||
Provider,
|
||||
CommerceProviderProps,
|
||||
CoreCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@vercel/commerce'
|
||||
import { spreeProvider } from './provider'
|
||||
import type { SpreeProvider } from './provider'
|
||||
import { SWRConfig } from 'swr'
|
||||
import handleTokenErrors from './utils/handle-token-errors'
|
||||
import useLogout from '@vercel/commerce/auth/use-logout'
|
||||
|
||||
export { spreeProvider }
|
||||
export type { SpreeProvider }
|
||||
|
||||
export const WithTokenErrorsHandling: FunctionComponent = ({ children }) => {
|
||||
const logout = useLogout()
|
||||
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
onError: (error, _key) => {
|
||||
handleTokenErrors(error, () => void logout())
|
||||
},
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</SWRConfig>
|
||||
)
|
||||
}
|
||||
|
||||
export const getCommerceProvider = <P extends Provider>(provider: P) => {
|
||||
return function CommerceProvider({
|
||||
children,
|
||||
...props
|
||||
}: CommerceProviderProps) {
|
||||
return (
|
||||
<CoreCommerceProvider provider={{ ...provider, ...props }}>
|
||||
<WithTokenErrorsHandling>{children}</WithTokenErrorsHandling>
|
||||
</CoreCommerceProvider>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const CommerceProvider =
|
||||
getCommerceProvider<SpreeProvider>(spreeProvider)
|
||||
|
||||
export const useCommerce = () => useCoreCommerce<SpreeProvider>()
|
81
packages/spree/src/isomorphic-config.ts
Normal file
81
packages/spree/src/isomorphic-config.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import forceIsomorphicConfigValues from './utils/force-isomorphic-config-values'
|
||||
import requireConfig from './utils/require-config'
|
||||
import validateAllProductsTaxonomyId from './utils/validations/validate-all-products-taxonomy-id'
|
||||
import validateCookieExpire from './utils/validations/validate-cookie-expire'
|
||||
import validateImagesOptionFilter from './utils/validations/validate-images-option-filter'
|
||||
import validatePlaceholderImageUrl from './utils/validations/validate-placeholder-image-url'
|
||||
import validateProductsPrerenderCount from './utils/validations/validate-products-prerender-count'
|
||||
import validateImagesSize from './utils/validations/validate-images-size'
|
||||
import validateImagesQuality from './utils/validations/validate-images-quality'
|
||||
|
||||
const isomorphicConfig = {
|
||||
apiHost: process.env.NEXT_PUBLIC_SPREE_API_HOST,
|
||||
defaultLocale: process.env.NEXT_PUBLIC_SPREE_DEFAULT_LOCALE,
|
||||
cartCookieName: process.env.NEXT_PUBLIC_SPREE_CART_COOKIE_NAME,
|
||||
cartCookieExpire: validateCookieExpire(
|
||||
process.env.NEXT_PUBLIC_SPREE_CART_COOKIE_EXPIRE
|
||||
),
|
||||
userCookieName: process.env.NEXT_PUBLIC_SPREE_USER_COOKIE_NAME,
|
||||
userCookieExpire: validateCookieExpire(
|
||||
process.env.NEXT_PUBLIC_SPREE_CART_COOKIE_EXPIRE
|
||||
),
|
||||
imageHost: process.env.NEXT_PUBLIC_SPREE_IMAGE_HOST,
|
||||
categoriesTaxonomyPermalink:
|
||||
process.env.NEXT_PUBLIC_SPREE_CATEGORIES_TAXONOMY_PERMALINK,
|
||||
brandsTaxonomyPermalink:
|
||||
process.env.NEXT_PUBLIC_SPREE_BRANDS_TAXONOMY_PERMALINK,
|
||||
allProductsTaxonomyId: validateAllProductsTaxonomyId(
|
||||
process.env.NEXT_PUBLIC_SPREE_ALL_PRODUCTS_TAXONOMY_ID
|
||||
),
|
||||
showSingleVariantOptions:
|
||||
process.env.NEXT_PUBLIC_SPREE_SHOW_SINGLE_VARIANT_OPTIONS === 'true',
|
||||
lastUpdatedProductsPrerenderCount: validateProductsPrerenderCount(
|
||||
process.env.NEXT_PUBLIC_SPREE_LAST_UPDATED_PRODUCTS_PRERENDER_COUNT
|
||||
),
|
||||
productPlaceholderImageUrl: validatePlaceholderImageUrl(
|
||||
process.env.NEXT_PUBLIC_SPREE_PRODUCT_PLACEHOLDER_IMAGE_URL
|
||||
),
|
||||
lineItemPlaceholderImageUrl: validatePlaceholderImageUrl(
|
||||
process.env.NEXT_PUBLIC_SPREE_LINE_ITEM_PLACEHOLDER_IMAGE_URL
|
||||
),
|
||||
imagesOptionFilter: validateImagesOptionFilter(
|
||||
process.env.NEXT_PUBLIC_SPREE_IMAGES_OPTION_FILTER
|
||||
),
|
||||
imagesSize: validateImagesSize(process.env.NEXT_PUBLIC_SPREE_IMAGES_SIZE),
|
||||
imagesQuality: validateImagesQuality(
|
||||
process.env.NEXT_PUBLIC_SPREE_IMAGES_QUALITY
|
||||
),
|
||||
loginAfterSignup: process.env.NEXT_PUBLIC_SPREE_LOGIN_AFTER_SIGNUP === 'true',
|
||||
}
|
||||
|
||||
export default forceIsomorphicConfigValues(
|
||||
isomorphicConfig,
|
||||
[],
|
||||
[
|
||||
'apiHost',
|
||||
'defaultLocale',
|
||||
'cartCookieName',
|
||||
'cartCookieExpire',
|
||||
'userCookieName',
|
||||
'userCookieExpire',
|
||||
'imageHost',
|
||||
'categoriesTaxonomyPermalink',
|
||||
'brandsTaxonomyPermalink',
|
||||
'allProductsTaxonomyId',
|
||||
'showSingleVariantOptions',
|
||||
'lastUpdatedProductsPrerenderCount',
|
||||
'productPlaceholderImageUrl',
|
||||
'lineItemPlaceholderImageUrl',
|
||||
'imagesOptionFilter',
|
||||
'imagesSize',
|
||||
'imagesQuality',
|
||||
'loginAfterSignup',
|
||||
]
|
||||
)
|
||||
|
||||
type IsomorphicConfig = typeof isomorphicConfig
|
||||
|
||||
const requireConfigValue = (key: keyof IsomorphicConfig) =>
|
||||
requireConfig<IsomorphicConfig>(isomorphicConfig, key)
|
||||
|
||||
export { requireConfigValue }
|
16
packages/spree/src/next.config.cjs
Normal file
16
packages/spree/src/next.config.cjs
Normal file
@@ -0,0 +1,16 @@
|
||||
const commerce = require('./commerce.config.json')
|
||||
|
||||
module.exports = {
|
||||
commerce,
|
||||
images: {
|
||||
domains: [process.env.NEXT_PUBLIC_SPREE_ALLOWED_IMAGE_DOMAIN],
|
||||
},
|
||||
rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/checkout',
|
||||
destination: '/api/checkout',
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
2
packages/spree/src/product/index.ts
Normal file
2
packages/spree/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/spree/src/product/use-price.tsx
Normal file
2
packages/spree/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'
|
104
packages/spree/src/product/use-search.tsx
Normal file
104
packages/spree/src/product/use-search.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import type { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useSearch from '@vercel/commerce/product/use-search'
|
||||
import type {
|
||||
Product,
|
||||
SearchProductsHook,
|
||||
} from '@vercel/commerce/types/product'
|
||||
import type { UseSearch } from '@vercel/commerce/product/use-search'
|
||||
import normalizeProduct from '../utils/normalizations/normalize-product'
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import { IProducts } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
||||
import { requireConfigValue } from '../isomorphic-config'
|
||||
|
||||
const imagesSize = requireConfigValue('imagesSize') as string
|
||||
const imagesQuality = requireConfigValue('imagesQuality') as number
|
||||
|
||||
const nextToSpreeSortMap: { [key: string]: string } = {
|
||||
'trending-desc': 'available_on',
|
||||
'latest-desc': 'updated_at',
|
||||
'price-asc': 'price',
|
||||
'price-desc': '-price',
|
||||
}
|
||||
|
||||
export const handler: SWRHook<SearchProductsHook> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
url: 'products',
|
||||
query: 'list',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
// This method is only needed if the options need to be modified before calling the generic fetcher (created in createFetcher).
|
||||
|
||||
console.info(
|
||||
'useSearch fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
const taxons = [input.categoryId, input.brandId].filter(Boolean)
|
||||
|
||||
const filter = {
|
||||
filter: {
|
||||
...(taxons.length > 0 ? { taxons: taxons.join(',') } : {}),
|
||||
...(input.search ? { name: input.search } : {}),
|
||||
},
|
||||
}
|
||||
|
||||
const sort = input.sort ? { sort: nextToSpreeSortMap[input.sort] } : {}
|
||||
|
||||
const { data: spreeSuccessResponse } = await fetch<
|
||||
GraphQLFetcherResult<IProducts>
|
||||
>({
|
||||
variables: {
|
||||
methodPath: 'products.list',
|
||||
arguments: [
|
||||
{},
|
||||
{
|
||||
include:
|
||||
'primary_variant,variants,images,option_types,variants.option_values',
|
||||
per_page: 50,
|
||||
...filter,
|
||||
...sort,
|
||||
image_transformation: {
|
||||
quality: imagesQuality,
|
||||
size: imagesSize,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
const normalizedProducts: Product[] = spreeSuccessResponse.data.map(
|
||||
(spreeProduct) => normalizeProduct(spreeSuccessResponse, spreeProduct)
|
||||
)
|
||||
|
||||
const found = spreeSuccessResponse.data.length > 0
|
||||
|
||||
return { products: normalizedProducts, found }
|
||||
},
|
||||
useHook: ({ useData }) => {
|
||||
const useWrappedHook: ReturnType<SWRHook<SearchProductsHook>['useHook']> = (
|
||||
input = {}
|
||||
) => {
|
||||
return useData({
|
||||
input: [
|
||||
['search', input.search],
|
||||
['categoryId', input.categoryId],
|
||||
['brandId', input.brandId],
|
||||
['sort', input.sort],
|
||||
],
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
// revalidateOnFocus: false means do not fetch products again when website is refocused in the web browser.
|
||||
...input.swrOptions,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
||||
|
||||
export default useSearch as UseSearch<typeof handler>
|
35
packages/spree/src/provider.ts
Normal file
35
packages/spree/src/provider.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
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'
|
||||
import { handler as useCheckout } from './checkout/use-checkout'
|
||||
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 { requireConfigValue } from './isomorphic-config'
|
||||
|
||||
const spreeProvider = {
|
||||
locale: requireConfigValue('defaultLocale') as string,
|
||||
cartCookie: requireConfigValue('cartCookieName') as string,
|
||||
fetcher,
|
||||
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||
customer: { useCustomer },
|
||||
products: { useSearch },
|
||||
auth: { useLogin, useLogout, useSignup },
|
||||
checkout: { useCheckout },
|
||||
wishlist: {
|
||||
useWishlist,
|
||||
useAddItem: useWishlistAddItem,
|
||||
useRemoveItem: useWishlistRemoveItem,
|
||||
},
|
||||
}
|
||||
|
||||
export { spreeProvider }
|
||||
|
||||
export type SpreeProvider = typeof spreeProvider
|
164
packages/spree/src/types/index.ts
Normal file
164
packages/spree/src/types/index.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import type { fetchResponseKey } from '../utils/create-customized-fetch-fetcher'
|
||||
import type {
|
||||
JsonApiDocument,
|
||||
JsonApiListResponse,
|
||||
JsonApiSingleResponse,
|
||||
} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi'
|
||||
import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse'
|
||||
import type { Response } from '@vercel/fetch'
|
||||
import type { ProductOption, Product } from '@vercel/commerce/types/product'
|
||||
import type {
|
||||
AddItemHook,
|
||||
RemoveItemHook,
|
||||
WishlistItemBody,
|
||||
WishlistTypes,
|
||||
} from '@vercel/commerce/types/wishlist'
|
||||
|
||||
export type UnknownObjectValues = Record<string, unknown>
|
||||
|
||||
export type NonUndefined<T> = T extends undefined ? never : T
|
||||
|
||||
export type ValueOf<T> = T[keyof T]
|
||||
|
||||
export type SpreeSdkResponse = JsonApiSingleResponse | JsonApiListResponse
|
||||
|
||||
export type SpreeSdkResponseWithRawResponse = SpreeSdkResponse & {
|
||||
[fetchResponseKey]: Response
|
||||
}
|
||||
|
||||
export type SpreeSdkResultResponseSuccessType = SpreeSdkResponseWithRawResponse
|
||||
|
||||
export type SpreeSdkMethodReturnType<
|
||||
ResultResponseSuccessType extends SpreeSdkResultResponseSuccessType = SpreeSdkResultResponseSuccessType
|
||||
> = Promise<ResultResponse<ResultResponseSuccessType>>
|
||||
|
||||
export type SpreeSdkMethod<
|
||||
ResultResponseSuccessType extends SpreeSdkResultResponseSuccessType = SpreeSdkResultResponseSuccessType
|
||||
> = (...args: any[]) => SpreeSdkMethodReturnType<ResultResponseSuccessType>
|
||||
|
||||
export type SpreeSdkVariables = {
|
||||
methodPath: string
|
||||
arguments: any[]
|
||||
}
|
||||
|
||||
export type FetcherVariables = SpreeSdkVariables & {
|
||||
refreshExpiredAccessToken: boolean
|
||||
replayUnauthorizedRequest: boolean
|
||||
}
|
||||
|
||||
export interface ImageStyle {
|
||||
url: string
|
||||
width: string
|
||||
height: string
|
||||
size: string
|
||||
}
|
||||
|
||||
export interface SpreeProductImage extends JsonApiDocument {
|
||||
attributes: {
|
||||
position: number
|
||||
alt: string
|
||||
original_url: string
|
||||
transformed_url: string | null
|
||||
styles: ImageStyle[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface OptionTypeAttr extends JsonApiDocument {
|
||||
attributes: {
|
||||
name: string
|
||||
presentation: string
|
||||
position: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
filterable: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface LineItemAttr extends JsonApiDocument {
|
||||
attributes: {
|
||||
name: string
|
||||
quantity: number
|
||||
slug: string
|
||||
options_text: string
|
||||
price: string
|
||||
currency: string
|
||||
display_price: string
|
||||
total: string
|
||||
display_total: string
|
||||
adjustment_total: string
|
||||
display_adjustment_total: string
|
||||
additional_tax_total: string
|
||||
display_additional_tax_total: string
|
||||
discounted_amount: string
|
||||
display_discounted_amount: string
|
||||
pre_tax_amount: string
|
||||
display_pre_tax_amount: string
|
||||
promo_total: string
|
||||
display_promo_total: string
|
||||
included_tax_total: string
|
||||
display_inluded_tax_total: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface VariantAttr extends JsonApiDocument {
|
||||
attributes: {
|
||||
sku: string
|
||||
price: string
|
||||
currency: string
|
||||
display_price: string
|
||||
weight: string
|
||||
height: string
|
||||
width: string
|
||||
depth: string
|
||||
is_master: boolean
|
||||
options_text: string
|
||||
purchasable: boolean
|
||||
in_stock: boolean
|
||||
backorderable: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProductSlugAttr extends JsonApiDocument {
|
||||
attributes: {
|
||||
slug: string
|
||||
}
|
||||
}
|
||||
export interface IProductsSlugs extends JsonApiListResponse {
|
||||
data: ProductSlugAttr[]
|
||||
}
|
||||
|
||||
export type ExpandedProductOption = ProductOption & { position: number }
|
||||
|
||||
export type UserOAuthTokens = {
|
||||
refreshToken: string
|
||||
accessToken: string
|
||||
}
|
||||
|
||||
// TODO: ExplicitCommerceWishlist is a temporary type
|
||||
// derived from tsx views. It will be removed once
|
||||
// Wishlist in @vercel/commerce/types/wishlist is updated
|
||||
// to a more specific type than `any`.
|
||||
export type ExplicitCommerceWishlist = {
|
||||
id: string
|
||||
token: string
|
||||
items: {
|
||||
id: string
|
||||
product_id: number
|
||||
variant_id: number
|
||||
product: Product
|
||||
}[]
|
||||
}
|
||||
|
||||
export type ExplicitWishlistAddItemHook = AddItemHook<
|
||||
WishlistTypes & {
|
||||
wishlist: ExplicitCommerceWishlist
|
||||
itemBody: WishlistItemBody & {
|
||||
wishlistToken?: string
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
export type ExplicitWishlistRemoveItemHook = RemoveItemHook & {
|
||||
fetcherInput: { wishlistToken?: string }
|
||||
body: { wishlistToken?: string }
|
||||
}
|
@@ -0,0 +1,52 @@
|
||||
import { FetcherError } from '@vercel/commerce/utils/errors'
|
||||
import { errors } from '@spree/storefront-api-v2-sdk'
|
||||
|
||||
const convertSpreeErrorToGraphQlError = (
|
||||
error: errors.SpreeError
|
||||
): FetcherError => {
|
||||
if (error instanceof errors.ExpandedSpreeError) {
|
||||
// Assuming error.errors[key] is a list of strings.
|
||||
|
||||
if ('base' in error.errors) {
|
||||
const baseErrorMessage = error.errors.base as unknown as string
|
||||
|
||||
return new FetcherError({
|
||||
status: error.serverResponse.status,
|
||||
message: baseErrorMessage,
|
||||
})
|
||||
}
|
||||
|
||||
const fetcherErrors = Object.keys(error.errors).map((sdkErrorKey) => {
|
||||
const errors = error.errors[sdkErrorKey] as string[]
|
||||
|
||||
// Naively assume sdkErrorKey is a label. Capitalize it for a better
|
||||
// out-of-the-box experience.
|
||||
const capitalizedSdkErrorKey = sdkErrorKey.replace(/^\w/, (firstChar) =>
|
||||
firstChar.toUpperCase()
|
||||
)
|
||||
|
||||
return {
|
||||
message: `${capitalizedSdkErrorKey} ${errors.join(', ')}`,
|
||||
}
|
||||
})
|
||||
|
||||
return new FetcherError({
|
||||
status: error.serverResponse.status,
|
||||
errors: fetcherErrors,
|
||||
})
|
||||
}
|
||||
|
||||
if (error instanceof errors.BasicSpreeError) {
|
||||
return new FetcherError({
|
||||
status: error.serverResponse.status,
|
||||
message: error.summary,
|
||||
})
|
||||
}
|
||||
|
||||
return new FetcherError({
|
||||
status: error.serverResponse.status,
|
||||
message: error.message,
|
||||
})
|
||||
}
|
||||
|
||||
export default convertSpreeErrorToGraphQlError
|
109
packages/spree/src/utils/create-customized-fetch-fetcher.ts
Normal file
109
packages/spree/src/utils/create-customized-fetch-fetcher.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
errors,
|
||||
request as spreeSdkRequestHelpers,
|
||||
} from '@spree/storefront-api-v2-sdk'
|
||||
import type { CreateCustomizedFetchFetcher } from '@spree/storefront-api-v2-sdk/types/interfaces/CreateCustomizedFetchFetcher'
|
||||
import isJsonContentType from './is-json-content-type'
|
||||
|
||||
export const fetchResponseKey = Symbol('fetch-response-key')
|
||||
|
||||
const createCustomizedFetchFetcher: CreateCustomizedFetchFetcher = (
|
||||
fetcherOptions
|
||||
) => {
|
||||
const { FetchError } = errors
|
||||
const sharedHeaders = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
|
||||
const { host, fetch, requestConstructor } = fetcherOptions
|
||||
|
||||
return {
|
||||
fetch: async (fetchOptions) => {
|
||||
// This fetcher always returns request equal null,
|
||||
// because @vercel/fetch doesn't accept a Request object as argument
|
||||
// and it's not used by NJC anyway.
|
||||
try {
|
||||
const { url, params, method, headers, responseParsing } = fetchOptions
|
||||
const absoluteUrl = new URL(url, host)
|
||||
let payload
|
||||
|
||||
switch (method.toUpperCase()) {
|
||||
case 'PUT':
|
||||
case 'POST':
|
||||
case 'DELETE':
|
||||
case 'PATCH':
|
||||
payload = { body: JSON.stringify(params) }
|
||||
break
|
||||
default:
|
||||
payload = null
|
||||
absoluteUrl.search =
|
||||
spreeSdkRequestHelpers.objectToQuerystring(params)
|
||||
}
|
||||
|
||||
const request: Request = new requestConstructor(
|
||||
absoluteUrl.toString(),
|
||||
{
|
||||
method: method.toUpperCase(),
|
||||
headers: { ...sharedHeaders, ...headers },
|
||||
...payload,
|
||||
}
|
||||
)
|
||||
|
||||
try {
|
||||
console.info(
|
||||
`Calling the Spree API: ${request.method} ${request.url}`
|
||||
)
|
||||
|
||||
const response: Response = await fetch(request)
|
||||
const responseContentType = response.headers.get('content-type')
|
||||
let data
|
||||
|
||||
if (responseParsing === 'automatic') {
|
||||
if (responseContentType && isJsonContentType(responseContentType)) {
|
||||
data = await response.json()
|
||||
} else {
|
||||
data = await response.text()
|
||||
}
|
||||
} else if (responseParsing === 'text') {
|
||||
data = await response.text()
|
||||
} else if (responseParsing === 'json') {
|
||||
data = await response.json()
|
||||
} else if (responseParsing === 'stream') {
|
||||
data = await response.body
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
// Use the "traditional" approach and reject non 2xx responses.
|
||||
throw new FetchError(response, request, data)
|
||||
}
|
||||
|
||||
data[fetchResponseKey] = response
|
||||
|
||||
return { data }
|
||||
} catch (error) {
|
||||
if (error instanceof FetchError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (!(error instanceof Error)) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new FetchError(null, request, null, error.message)
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof FetchError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
if (!(error instanceof Error)) {
|
||||
throw error
|
||||
}
|
||||
|
||||
throw new FetchError(null, null, null, error.message)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default createCustomizedFetchFetcher
|
22
packages/spree/src/utils/create-empty-cart.ts
Normal file
22
packages/spree/src/utils/create-empty-cart.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import type { HookFetcherContext } from '@vercel/commerce/utils/types'
|
||||
import type { IOrder } from '@spree/storefront-api-v2-sdk/types/interfaces/Order'
|
||||
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import ensureIToken from './tokens/ensure-itoken'
|
||||
|
||||
const createEmptyCart = (
|
||||
fetch: HookFetcherContext<{
|
||||
data: any
|
||||
}>['fetch']
|
||||
): Promise<GraphQLFetcherResult<IOrder>> => {
|
||||
const token: IToken | undefined = ensureIToken()
|
||||
|
||||
return fetch<GraphQLFetcherResult<IOrder>>({
|
||||
variables: {
|
||||
methodPath: 'cart.create',
|
||||
arguments: [token],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export default createEmptyCart
|
26
packages/spree/src/utils/create-get-absolute-image-url.ts
Normal file
26
packages/spree/src/utils/create-get-absolute-image-url.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { SpreeProductImage } from '../types'
|
||||
import getImageUrl from './get-image-url'
|
||||
|
||||
const createGetAbsoluteImageUrl =
|
||||
(host: string, useOriginalImageSize: boolean = true) =>
|
||||
(
|
||||
image: SpreeProductImage,
|
||||
minWidth: number,
|
||||
minHeight: number
|
||||
): string | null => {
|
||||
let url
|
||||
|
||||
if (useOriginalImageSize) {
|
||||
url = image.attributes.transformed_url || null
|
||||
} else {
|
||||
url = getImageUrl(image, minWidth, minHeight)
|
||||
}
|
||||
|
||||
if (url === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
return `${host}${url}`
|
||||
}
|
||||
|
||||
export default createGetAbsoluteImageUrl
|
103
packages/spree/src/utils/expand-options.ts
Normal file
103
packages/spree/src/utils/expand-options.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { ProductOptionValues } from '@vercel/commerce/types/product'
|
||||
import type {
|
||||
JsonApiDocument,
|
||||
JsonApiResponse,
|
||||
} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi'
|
||||
import { jsonApi } from '@spree/storefront-api-v2-sdk'
|
||||
import type { RelationType } from '@spree/storefront-api-v2-sdk/types/interfaces/Relationships'
|
||||
import SpreeResponseContentError from '../errors/SpreeResponseContentError'
|
||||
import type { OptionTypeAttr, ExpandedProductOption } from '../types'
|
||||
import sortOptionsByPosition from '../utils/sort-option-types'
|
||||
|
||||
const isColorProductOption = (productOption: ExpandedProductOption) => {
|
||||
return productOption.displayName === 'Color'
|
||||
}
|
||||
|
||||
const expandOptions = (
|
||||
spreeSuccessResponse: JsonApiResponse,
|
||||
spreeOptionValue: JsonApiDocument,
|
||||
accumulatedOptions: ExpandedProductOption[]
|
||||
): ExpandedProductOption[] => {
|
||||
const spreeOptionTypeIdentifier = spreeOptionValue.relationships.option_type
|
||||
.data as RelationType
|
||||
|
||||
const existingOptionIndex = accumulatedOptions.findIndex(
|
||||
(option) => option.id == spreeOptionTypeIdentifier.id
|
||||
)
|
||||
|
||||
let option: ExpandedProductOption
|
||||
|
||||
if (existingOptionIndex === -1) {
|
||||
const spreeOptionType = jsonApi.findDocument<OptionTypeAttr>(
|
||||
spreeSuccessResponse,
|
||||
spreeOptionTypeIdentifier
|
||||
)
|
||||
|
||||
if (!spreeOptionType) {
|
||||
throw new SpreeResponseContentError(
|
||||
`Option type with id ${spreeOptionTypeIdentifier.id} not found.`
|
||||
)
|
||||
}
|
||||
|
||||
option = {
|
||||
__typename: 'MultipleChoiceOption',
|
||||
id: spreeOptionType.id,
|
||||
displayName: spreeOptionType.attributes.presentation,
|
||||
position: spreeOptionType.attributes.position,
|
||||
values: [],
|
||||
}
|
||||
} else {
|
||||
const existingOption = accumulatedOptions[existingOptionIndex]
|
||||
|
||||
option = existingOption
|
||||
}
|
||||
|
||||
let optionValue: ProductOptionValues
|
||||
|
||||
const label = isColorProductOption(option)
|
||||
? spreeOptionValue.attributes.name
|
||||
: spreeOptionValue.attributes.presentation
|
||||
|
||||
const productOptionValueExists = option.values.some(
|
||||
(optionValue: ProductOptionValues) => optionValue.label === label
|
||||
)
|
||||
|
||||
if (!productOptionValueExists) {
|
||||
if (isColorProductOption(option)) {
|
||||
optionValue = {
|
||||
label,
|
||||
hexColors: [spreeOptionValue.attributes.presentation],
|
||||
}
|
||||
} else {
|
||||
optionValue = {
|
||||
label,
|
||||
}
|
||||
}
|
||||
|
||||
if (existingOptionIndex === -1) {
|
||||
return [
|
||||
...accumulatedOptions,
|
||||
{
|
||||
...option,
|
||||
values: [optionValue],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
const expandedOptionValues = [...option.values, optionValue]
|
||||
const expandedOptions = [...accumulatedOptions]
|
||||
|
||||
expandedOptions[existingOptionIndex] = {
|
||||
...option,
|
||||
values: expandedOptionValues,
|
||||
}
|
||||
|
||||
const sortedOptions = sortOptionsByPosition(expandedOptions)
|
||||
|
||||
return sortedOptions
|
||||
}
|
||||
|
||||
return accumulatedOptions
|
||||
}
|
||||
|
||||
export default expandOptions
|
43
packages/spree/src/utils/force-isomorphic-config-values.ts
Normal file
43
packages/spree/src/utils/force-isomorphic-config-values.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { NonUndefined, UnknownObjectValues } from '../types'
|
||||
import MisconfigurationError from '../errors/MisconfigurationError'
|
||||
import isServer from './is-server'
|
||||
|
||||
const generateMisconfigurationErrorMessage = (
|
||||
keys: Array<string | number | symbol>
|
||||
) => `${keys.join(', ')} must have a value before running the Framework.`
|
||||
|
||||
const forceIsomorphicConfigValues = <
|
||||
X extends keyof T,
|
||||
T extends UnknownObjectValues,
|
||||
H extends Record<X, NonUndefined<T[X]>>
|
||||
>(
|
||||
config: T,
|
||||
requiredServerKeys: string[],
|
||||
requiredPublicKeys: X[]
|
||||
) => {
|
||||
if (isServer) {
|
||||
const missingServerConfigValues = requiredServerKeys.filter(
|
||||
(requiredServerKey) => typeof config[requiredServerKey] === 'undefined'
|
||||
)
|
||||
|
||||
if (missingServerConfigValues.length > 0) {
|
||||
throw new MisconfigurationError(
|
||||
generateMisconfigurationErrorMessage(missingServerConfigValues)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const missingPublicConfigValues = requiredPublicKeys.filter(
|
||||
(requiredPublicKey) => typeof config[requiredPublicKey] === 'undefined'
|
||||
)
|
||||
|
||||
if (missingPublicConfigValues.length > 0) {
|
||||
throw new MisconfigurationError(
|
||||
generateMisconfigurationErrorMessage(missingPublicConfigValues)
|
||||
)
|
||||
}
|
||||
|
||||
return config as T & H
|
||||
}
|
||||
|
||||
export default forceIsomorphicConfigValues
|
44
packages/spree/src/utils/get-image-url.ts
Normal file
44
packages/spree/src/utils/get-image-url.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
// Based on https://github.com/spark-solutions/spree2vuestorefront/blob/d88d85ae1bcd2ec99b13b81cd2e3c25600a0216e/src/utils/index.ts
|
||||
|
||||
import type { ImageStyle, SpreeProductImage } from '../types'
|
||||
|
||||
const getImageUrl = (
|
||||
image: SpreeProductImage,
|
||||
minWidth: number,
|
||||
_: number
|
||||
): string | null => {
|
||||
// every image is still resized in vue-storefront-api, no matter what getImageUrl returns
|
||||
if (image) {
|
||||
const {
|
||||
attributes: { styles },
|
||||
} = image
|
||||
const bestStyleIndex = styles.reduce(
|
||||
(bSIndex: number | null, style: ImageStyle, styleIndex: number) => {
|
||||
// assuming all images are the same dimensions, just scaled
|
||||
if (bSIndex === null) {
|
||||
return 0
|
||||
}
|
||||
const bestStyle = styles[bSIndex]
|
||||
const widthDiff = +bestStyle.width - minWidth
|
||||
const minWidthDiff = +style.width - minWidth
|
||||
if (widthDiff < 0 && minWidthDiff > 0) {
|
||||
return styleIndex
|
||||
}
|
||||
if (widthDiff > 0 && minWidthDiff < 0) {
|
||||
return bSIndex
|
||||
}
|
||||
return Math.abs(widthDiff) < Math.abs(minWidthDiff)
|
||||
? bSIndex
|
||||
: styleIndex
|
||||
},
|
||||
null
|
||||
)
|
||||
|
||||
if (bestStyleIndex !== null) {
|
||||
return styles[bestStyleIndex].url
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export default getImageUrl
|
25
packages/spree/src/utils/get-media-gallery.ts
Normal file
25
packages/spree/src/utils/get-media-gallery.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
// Based on https://github.com/spark-solutions/spree2vuestorefront/blob/d88d85ae1bcd2ec99b13b81cd2e3c25600a0216e/src/utils/index.ts
|
||||
|
||||
import type { ProductImage } from '@vercel/commerce/types/product'
|
||||
import type { SpreeProductImage } from '../types'
|
||||
|
||||
const getMediaGallery = (
|
||||
images: SpreeProductImage[],
|
||||
getImageUrl: (
|
||||
image: SpreeProductImage,
|
||||
minWidth: number,
|
||||
minHeight: number
|
||||
) => string | null
|
||||
) => {
|
||||
return images.reduce<ProductImage[]>((productImages, _, imageIndex) => {
|
||||
const url = getImageUrl(images[imageIndex], 800, 800)
|
||||
|
||||
if (url) {
|
||||
return [...productImages, { url }]
|
||||
}
|
||||
|
||||
return productImages
|
||||
}, [])
|
||||
}
|
||||
|
||||
export default getMediaGallery
|
7
packages/spree/src/utils/get-product-path.ts
Normal file
7
packages/spree/src/utils/get-product-path.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { ProductSlugAttr } from '../types'
|
||||
|
||||
const getProductPath = (partialSpreeProduct: ProductSlugAttr) => {
|
||||
return `/${partialSpreeProduct.attributes.slug}`
|
||||
}
|
||||
|
||||
export default getProductPath
|
@@ -0,0 +1,61 @@
|
||||
import type { Client } from '@spree/storefront-api-v2-sdk'
|
||||
import SpreeSdkMethodFromEndpointPathError from '../errors/SpreeSdkMethodFromEndpointPathError'
|
||||
import type {
|
||||
SpreeSdkMethod,
|
||||
SpreeSdkResultResponseSuccessType,
|
||||
} from '../types'
|
||||
|
||||
const getSpreeSdkMethodFromEndpointPath = <
|
||||
ExactSpreeSdkClientType extends Client,
|
||||
ResultResponseSuccessType extends SpreeSdkResultResponseSuccessType = SpreeSdkResultResponseSuccessType
|
||||
>(
|
||||
client: ExactSpreeSdkClientType,
|
||||
path: string
|
||||
): SpreeSdkMethod<ResultResponseSuccessType> => {
|
||||
const pathParts = path.split('.')
|
||||
const reachedPath: string[] = []
|
||||
let node = <Record<string, unknown>>client
|
||||
|
||||
console.log(`Looking for ${path} in Spree Sdk.`)
|
||||
|
||||
while (reachedPath.length < pathParts.length - 1) {
|
||||
const checkedPathPart = pathParts[reachedPath.length]
|
||||
const checkedNode = node[checkedPathPart]
|
||||
|
||||
console.log(`Checking part ${checkedPathPart}.`)
|
||||
|
||||
if (typeof checkedNode !== 'object') {
|
||||
throw new SpreeSdkMethodFromEndpointPathError(
|
||||
`Couldn't reach ${path}. Farthest path reached was: ${reachedPath.join(
|
||||
'.'
|
||||
)}.`
|
||||
)
|
||||
}
|
||||
|
||||
if (checkedNode === null) {
|
||||
throw new SpreeSdkMethodFromEndpointPathError(
|
||||
`Path ${path} doesn't exist.`
|
||||
)
|
||||
}
|
||||
|
||||
node = <Record<string, unknown>>checkedNode
|
||||
reachedPath.push(checkedPathPart)
|
||||
}
|
||||
|
||||
const foundEndpointMethod = node[pathParts[reachedPath.length]]
|
||||
|
||||
if (
|
||||
reachedPath.length !== pathParts.length - 1 ||
|
||||
typeof foundEndpointMethod !== 'function'
|
||||
) {
|
||||
throw new SpreeSdkMethodFromEndpointPathError(
|
||||
`Couldn't reach ${path}. Farthest path reached was: ${reachedPath.join(
|
||||
'.'
|
||||
)}.`
|
||||
)
|
||||
}
|
||||
|
||||
return foundEndpointMethod.bind(node)
|
||||
}
|
||||
|
||||
export default getSpreeSdkMethodFromEndpointPath
|
14
packages/spree/src/utils/handle-token-errors.ts
Normal file
14
packages/spree/src/utils/handle-token-errors.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import AccessTokenError from '../errors/AccessTokenError'
|
||||
import RefreshTokenError from '../errors/RefreshTokenError'
|
||||
|
||||
const handleTokenErrors = (error: unknown, action: () => void): boolean => {
|
||||
if (error instanceof AccessTokenError || error instanceof RefreshTokenError) {
|
||||
action()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export default handleTokenErrors
|
5
packages/spree/src/utils/is-json-content-type.ts
Normal file
5
packages/spree/src/utils/is-json-content-type.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const isJsonContentType = (contentType: string): boolean =>
|
||||
contentType.includes('application/json') ||
|
||||
contentType.includes('application/vnd.api+json')
|
||||
|
||||
export default isJsonContentType
|
1
packages/spree/src/utils/is-server.ts
Normal file
1
packages/spree/src/utils/is-server.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default typeof window === 'undefined'
|
58
packages/spree/src/utils/login.ts
Normal file
58
packages/spree/src/utils/login.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import type { HookFetcherContext } from '@vercel/commerce/utils/types'
|
||||
import type { AuthTokenAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Authentication'
|
||||
import type { AssociateCart } from '@spree/storefront-api-v2-sdk/types/interfaces/endpoints/CartClass'
|
||||
import type { IOrder } from '@spree/storefront-api-v2-sdk/types/interfaces/Order'
|
||||
import type {
|
||||
IOAuthToken,
|
||||
IToken,
|
||||
} from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import { getCartToken, removeCartToken } from './tokens/cart-token'
|
||||
import { setUserTokenResponse } from './tokens/user-token-response'
|
||||
|
||||
const login = async (
|
||||
fetch: HookFetcherContext<{
|
||||
data: any
|
||||
}>['fetch'],
|
||||
getTokenParameters: AuthTokenAttr,
|
||||
associateGuestCart: boolean
|
||||
): Promise<void> => {
|
||||
const { data: spreeGetTokenSuccessResponse } = await fetch<
|
||||
GraphQLFetcherResult<IOAuthToken>
|
||||
>({
|
||||
variables: {
|
||||
methodPath: 'authentication.getToken',
|
||||
arguments: [getTokenParameters],
|
||||
},
|
||||
})
|
||||
|
||||
setUserTokenResponse(spreeGetTokenSuccessResponse)
|
||||
|
||||
if (associateGuestCart) {
|
||||
const cartToken = getCartToken()
|
||||
|
||||
if (cartToken) {
|
||||
// If the user had a cart as guest still use its contents
|
||||
// after logging in.
|
||||
const accessToken = spreeGetTokenSuccessResponse.access_token
|
||||
const token: IToken = { bearerToken: accessToken }
|
||||
|
||||
const associateGuestCartParameters: AssociateCart = {
|
||||
guest_order_token: cartToken,
|
||||
}
|
||||
|
||||
await fetch<GraphQLFetcherResult<IOrder>>({
|
||||
variables: {
|
||||
methodPath: 'cart.associateGuestCart',
|
||||
arguments: [token, associateGuestCartParameters],
|
||||
},
|
||||
})
|
||||
|
||||
// We no longer need the guest cart token, so let's remove it.
|
||||
}
|
||||
}
|
||||
|
||||
removeCartToken()
|
||||
}
|
||||
|
||||
export default login
|
211
packages/spree/src/utils/normalizations/normalize-cart.ts
Normal file
211
packages/spree/src/utils/normalizations/normalize-cart.ts
Normal file
@@ -0,0 +1,211 @@
|
||||
import type {
|
||||
Cart,
|
||||
LineItem,
|
||||
ProductVariant,
|
||||
SelectedOption,
|
||||
} from '@vercel/commerce/types/cart'
|
||||
import MissingLineItemVariantError from '../../errors/MissingLineItemVariantError'
|
||||
import { requireConfigValue } from '../../isomorphic-config'
|
||||
import type { OrderAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Order'
|
||||
import type { ProductAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
||||
import type { Image } from '@vercel/commerce/types/common'
|
||||
import { jsonApi } from '@spree/storefront-api-v2-sdk'
|
||||
import createGetAbsoluteImageUrl from '../create-get-absolute-image-url'
|
||||
import getMediaGallery from '../get-media-gallery'
|
||||
import type {
|
||||
LineItemAttr,
|
||||
OptionTypeAttr,
|
||||
SpreeProductImage,
|
||||
SpreeSdkResponse,
|
||||
VariantAttr,
|
||||
} from '../../types'
|
||||
|
||||
const placeholderImage = requireConfigValue('lineItemPlaceholderImageUrl') as
|
||||
| string
|
||||
| false
|
||||
|
||||
const isColorProductOption = (productOptionType: OptionTypeAttr) => {
|
||||
return productOptionType.attributes.presentation === 'Color'
|
||||
}
|
||||
|
||||
const normalizeVariant = (
|
||||
spreeSuccessResponse: SpreeSdkResponse,
|
||||
spreeVariant: VariantAttr
|
||||
): ProductVariant => {
|
||||
const spreeProduct = jsonApi.findSingleRelationshipDocument<ProductAttr>(
|
||||
spreeSuccessResponse,
|
||||
spreeVariant,
|
||||
'product'
|
||||
)
|
||||
|
||||
if (spreeProduct === null) {
|
||||
throw new MissingLineItemVariantError(
|
||||
`Couldn't find product for variant with id ${spreeVariant.id}.`
|
||||
)
|
||||
}
|
||||
|
||||
const spreeVariantImageRecords =
|
||||
jsonApi.findRelationshipDocuments<SpreeProductImage>(
|
||||
spreeSuccessResponse,
|
||||
spreeVariant,
|
||||
'images'
|
||||
)
|
||||
|
||||
let lineItemImage
|
||||
|
||||
const variantImage = getMediaGallery(
|
||||
spreeVariantImageRecords,
|
||||
createGetAbsoluteImageUrl(requireConfigValue('imageHost') as string)
|
||||
)[0]
|
||||
|
||||
if (variantImage) {
|
||||
lineItemImage = variantImage
|
||||
} else {
|
||||
const spreeProductImageRecords =
|
||||
jsonApi.findRelationshipDocuments<SpreeProductImage>(
|
||||
spreeSuccessResponse,
|
||||
spreeProduct,
|
||||
'images'
|
||||
)
|
||||
|
||||
const productImage = getMediaGallery(
|
||||
spreeProductImageRecords,
|
||||
createGetAbsoluteImageUrl(requireConfigValue('imageHost') as string)
|
||||
)[0]
|
||||
|
||||
lineItemImage = productImage
|
||||
}
|
||||
|
||||
const image: Image =
|
||||
lineItemImage ??
|
||||
(placeholderImage === false ? undefined : { url: placeholderImage })
|
||||
|
||||
return {
|
||||
id: spreeVariant.id,
|
||||
sku: spreeVariant.attributes.sku,
|
||||
name: spreeProduct.attributes.name,
|
||||
requiresShipping: true,
|
||||
price: parseFloat(spreeVariant.attributes.price),
|
||||
listPrice: parseFloat(spreeVariant.attributes.price),
|
||||
image,
|
||||
isInStock: spreeVariant.attributes.in_stock,
|
||||
availableForSale: spreeVariant.attributes.purchasable,
|
||||
...(spreeVariant.attributes.weight === '0.0'
|
||||
? {}
|
||||
: {
|
||||
weight: {
|
||||
value: parseFloat(spreeVariant.attributes.weight),
|
||||
unit: 'KILOGRAMS',
|
||||
},
|
||||
}),
|
||||
// TODO: Add height, width and depth when Measurement type allows distance measurements.
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeLineItem = (
|
||||
spreeSuccessResponse: SpreeSdkResponse,
|
||||
spreeLineItem: LineItemAttr
|
||||
): LineItem => {
|
||||
const variant = jsonApi.findSingleRelationshipDocument<VariantAttr>(
|
||||
spreeSuccessResponse,
|
||||
spreeLineItem,
|
||||
'variant'
|
||||
)
|
||||
|
||||
if (variant === null) {
|
||||
throw new MissingLineItemVariantError(
|
||||
`Couldn't find variant for line item with id ${spreeLineItem.id}.`
|
||||
)
|
||||
}
|
||||
|
||||
const product = jsonApi.findSingleRelationshipDocument<ProductAttr>(
|
||||
spreeSuccessResponse,
|
||||
variant,
|
||||
'product'
|
||||
)
|
||||
|
||||
if (product === null) {
|
||||
throw new MissingLineItemVariantError(
|
||||
`Couldn't find product for variant with id ${variant.id}.`
|
||||
)
|
||||
}
|
||||
|
||||
// CartItem.tsx expects path without a '/' prefix unlike pages/product/[slug].tsx and others.
|
||||
const path = `${product.attributes.slug}`
|
||||
|
||||
const spreeOptionValues = jsonApi.findRelationshipDocuments(
|
||||
spreeSuccessResponse,
|
||||
variant,
|
||||
'option_values'
|
||||
)
|
||||
|
||||
const options: SelectedOption[] = spreeOptionValues.map(
|
||||
(spreeOptionValue) => {
|
||||
const spreeOptionType =
|
||||
jsonApi.findSingleRelationshipDocument<OptionTypeAttr>(
|
||||
spreeSuccessResponse,
|
||||
spreeOptionValue,
|
||||
'option_type'
|
||||
)
|
||||
|
||||
if (spreeOptionType === null) {
|
||||
throw new MissingLineItemVariantError(
|
||||
`Couldn't find option type of option value with id ${spreeOptionValue.id}.`
|
||||
)
|
||||
}
|
||||
|
||||
const label = isColorProductOption(spreeOptionType)
|
||||
? spreeOptionValue.attributes.name
|
||||
: spreeOptionValue.attributes.presentation
|
||||
|
||||
return {
|
||||
id: spreeOptionValue.id,
|
||||
name: spreeOptionType.attributes.presentation,
|
||||
value: label,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
id: spreeLineItem.id,
|
||||
variantId: variant.id,
|
||||
productId: product.id,
|
||||
name: spreeLineItem.attributes.name,
|
||||
quantity: spreeLineItem.attributes.quantity,
|
||||
discounts: [], // TODO: Implement when the template starts displaying them.
|
||||
path,
|
||||
variant: normalizeVariant(spreeSuccessResponse, variant),
|
||||
options,
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeCart = (
|
||||
spreeSuccessResponse: SpreeSdkResponse,
|
||||
spreeCart: OrderAttr
|
||||
): Cart => {
|
||||
const lineItems = jsonApi
|
||||
.findRelationshipDocuments<LineItemAttr>(
|
||||
spreeSuccessResponse,
|
||||
spreeCart,
|
||||
'line_items'
|
||||
)
|
||||
.map((lineItem) => normalizeLineItem(spreeSuccessResponse, lineItem))
|
||||
|
||||
return {
|
||||
id: spreeCart.id,
|
||||
createdAt: spreeCart.attributes.created_at.toString(),
|
||||
currency: { code: spreeCart.attributes.currency },
|
||||
taxesIncluded: true,
|
||||
lineItems,
|
||||
lineItemsSubtotalPrice: parseFloat(spreeCart.attributes.item_total),
|
||||
subtotalPrice: parseFloat(spreeCart.attributes.item_total),
|
||||
totalPrice: parseFloat(spreeCart.attributes.total),
|
||||
customerId: spreeCart.attributes.token,
|
||||
email: spreeCart.attributes.email,
|
||||
discounts: [], // TODO: Implement when the template starts displaying them.
|
||||
}
|
||||
}
|
||||
|
||||
export { normalizeLineItem }
|
||||
|
||||
export default normalizeCart
|
42
packages/spree/src/utils/normalizations/normalize-page.ts
Normal file
42
packages/spree/src/utils/normalizations/normalize-page.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Page } from '@vercel/commerce/types/page'
|
||||
import type { PageAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Page'
|
||||
import { SpreeSdkResponse } from '../../types'
|
||||
|
||||
const normalizePage = (
|
||||
_spreeSuccessResponse: SpreeSdkResponse,
|
||||
spreePage: PageAttr,
|
||||
commerceLocales: string[]
|
||||
): Page => {
|
||||
// If the locale returned by Spree is not available, search
|
||||
// for a similar one.
|
||||
|
||||
const spreeLocale = spreePage.attributes.locale
|
||||
let usedCommerceLocale: string
|
||||
|
||||
if (commerceLocales.includes(spreeLocale)) {
|
||||
usedCommerceLocale = spreeLocale
|
||||
} else {
|
||||
const genericSpreeLocale = spreeLocale.split('-')[0]
|
||||
|
||||
const foundExactGenericLocale = commerceLocales.includes(genericSpreeLocale)
|
||||
|
||||
if (foundExactGenericLocale) {
|
||||
usedCommerceLocale = genericSpreeLocale
|
||||
} else {
|
||||
const foundSimilarLocale = commerceLocales.find((locale) => {
|
||||
return locale.split('-')[0] === genericSpreeLocale
|
||||
})
|
||||
|
||||
usedCommerceLocale = foundSimilarLocale || spreeLocale
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: spreePage.id,
|
||||
name: spreePage.attributes.title,
|
||||
url: `/${usedCommerceLocale}/${spreePage.attributes.slug}`,
|
||||
body: spreePage.attributes.content,
|
||||
}
|
||||
}
|
||||
|
||||
export default normalizePage
|
240
packages/spree/src/utils/normalizations/normalize-product.ts
Normal file
240
packages/spree/src/utils/normalizations/normalize-product.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
import type {
|
||||
Product,
|
||||
ProductImage,
|
||||
ProductPrice,
|
||||
ProductVariant,
|
||||
} from '@vercel/commerce/types/product'
|
||||
import type { ProductAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
||||
import type { RelationType } from '@spree/storefront-api-v2-sdk/types/interfaces/Relationships'
|
||||
import { jsonApi } from '@spree/storefront-api-v2-sdk'
|
||||
import { JsonApiDocument } from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi'
|
||||
import { requireConfigValue } from '../../isomorphic-config'
|
||||
import createGetAbsoluteImageUrl from '../create-get-absolute-image-url'
|
||||
import expandOptions from '../expand-options'
|
||||
import getMediaGallery from '../get-media-gallery'
|
||||
import getProductPath from '../get-product-path'
|
||||
import MissingPrimaryVariantError from '../../errors/MissingPrimaryVariantError'
|
||||
import MissingOptionValueError from '../../errors/MissingOptionValueError'
|
||||
import type {
|
||||
ExpandedProductOption,
|
||||
SpreeSdkResponse,
|
||||
VariantAttr,
|
||||
} from '../../types'
|
||||
|
||||
const placeholderImage = requireConfigValue('productPlaceholderImageUrl') as
|
||||
| string
|
||||
| false
|
||||
|
||||
const imagesOptionFilter = requireConfigValue('imagesOptionFilter') as
|
||||
| string
|
||||
| false
|
||||
|
||||
const normalizeProduct = (
|
||||
spreeSuccessResponse: SpreeSdkResponse,
|
||||
spreeProduct: ProductAttr
|
||||
): Product => {
|
||||
const spreePrimaryVariant =
|
||||
jsonApi.findSingleRelationshipDocument<VariantAttr>(
|
||||
spreeSuccessResponse,
|
||||
spreeProduct,
|
||||
'primary_variant'
|
||||
)
|
||||
|
||||
if (spreePrimaryVariant === null) {
|
||||
throw new MissingPrimaryVariantError(
|
||||
`Couldn't find primary variant for product with id ${spreeProduct.id}.`
|
||||
)
|
||||
}
|
||||
|
||||
const sku = spreePrimaryVariant.attributes.sku
|
||||
|
||||
const price: ProductPrice = {
|
||||
value: parseFloat(spreeProduct.attributes.price),
|
||||
currencyCode: spreeProduct.attributes.currency,
|
||||
}
|
||||
|
||||
const hasNonMasterVariants =
|
||||
(spreeProduct.relationships.variants.data as RelationType[]).length > 1
|
||||
|
||||
const showOptions =
|
||||
(requireConfigValue('showSingleVariantOptions') as boolean) ||
|
||||
hasNonMasterVariants
|
||||
|
||||
let options: ExpandedProductOption[] = []
|
||||
|
||||
const spreeVariantRecords = jsonApi.findRelationshipDocuments(
|
||||
spreeSuccessResponse,
|
||||
spreeProduct,
|
||||
'variants'
|
||||
)
|
||||
|
||||
// Use variants with option values if available. Fall back to
|
||||
// Spree primary_variant if no explicit variants are present.
|
||||
const spreeOptionsVariantsOrPrimary =
|
||||
spreeVariantRecords.length === 0
|
||||
? [spreePrimaryVariant]
|
||||
: spreeVariantRecords
|
||||
|
||||
const variants: ProductVariant[] = spreeOptionsVariantsOrPrimary.map(
|
||||
(spreeVariantRecord) => {
|
||||
let variantOptions: ExpandedProductOption[] = []
|
||||
|
||||
if (showOptions) {
|
||||
const spreeOptionValues = jsonApi.findRelationshipDocuments(
|
||||
spreeSuccessResponse,
|
||||
spreeVariantRecord,
|
||||
'option_values'
|
||||
)
|
||||
|
||||
// Only include options which are used by variants.
|
||||
|
||||
spreeOptionValues.forEach((spreeOptionValue) => {
|
||||
variantOptions = expandOptions(
|
||||
spreeSuccessResponse,
|
||||
spreeOptionValue,
|
||||
variantOptions
|
||||
)
|
||||
|
||||
options = expandOptions(
|
||||
spreeSuccessResponse,
|
||||
spreeOptionValue,
|
||||
options
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
id: spreeVariantRecord.id,
|
||||
options: variantOptions,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const spreePrimaryVariantImageRecords = jsonApi.findRelationshipDocuments(
|
||||
spreeSuccessResponse,
|
||||
spreePrimaryVariant,
|
||||
'images'
|
||||
)
|
||||
|
||||
let spreeVariantImageRecords: JsonApiDocument[]
|
||||
|
||||
if (imagesOptionFilter === false) {
|
||||
spreeVariantImageRecords = spreeVariantRecords.reduce<JsonApiDocument[]>(
|
||||
(accumulatedImageRecords, spreeVariantRecord) => {
|
||||
return [
|
||||
...accumulatedImageRecords,
|
||||
...jsonApi.findRelationshipDocuments(
|
||||
spreeSuccessResponse,
|
||||
spreeVariantRecord,
|
||||
'images'
|
||||
),
|
||||
]
|
||||
},
|
||||
[]
|
||||
)
|
||||
} else {
|
||||
const spreeOptionTypes = jsonApi.findRelationshipDocuments(
|
||||
spreeSuccessResponse,
|
||||
spreeProduct,
|
||||
'option_types'
|
||||
)
|
||||
|
||||
const imagesFilterOptionType = spreeOptionTypes.find(
|
||||
(spreeOptionType) =>
|
||||
spreeOptionType.attributes.name === imagesOptionFilter
|
||||
)
|
||||
|
||||
if (!imagesFilterOptionType) {
|
||||
console.warn(
|
||||
`Couldn't find option type having name ${imagesOptionFilter} for product with id ${spreeProduct.id}.` +
|
||||
' Showing no images for this product.'
|
||||
)
|
||||
|
||||
spreeVariantImageRecords = []
|
||||
} else {
|
||||
const imagesOptionTypeFilterId = imagesFilterOptionType.id
|
||||
const includedOptionValuesImagesIds: string[] = []
|
||||
|
||||
spreeVariantImageRecords = spreeVariantRecords.reduce<JsonApiDocument[]>(
|
||||
(accumulatedImageRecords, spreeVariantRecord) => {
|
||||
const spreeVariantOptionValuesIdentifiers: RelationType[] =
|
||||
spreeVariantRecord.relationships.option_values.data
|
||||
|
||||
const spreeOptionValueOfFilterTypeIdentifier =
|
||||
spreeVariantOptionValuesIdentifiers.find(
|
||||
(spreeVariantOptionValuesIdentifier: RelationType) =>
|
||||
imagesFilterOptionType.relationships.option_values.data.some(
|
||||
(filterOptionTypeValueIdentifier: RelationType) =>
|
||||
filterOptionTypeValueIdentifier.id ===
|
||||
spreeVariantOptionValuesIdentifier.id
|
||||
)
|
||||
)
|
||||
|
||||
if (!spreeOptionValueOfFilterTypeIdentifier) {
|
||||
throw new MissingOptionValueError(
|
||||
`Couldn't find option value related to option type with id ${imagesOptionTypeFilterId}.`
|
||||
)
|
||||
}
|
||||
|
||||
const optionValueImagesAlreadyIncluded =
|
||||
includedOptionValuesImagesIds.includes(
|
||||
spreeOptionValueOfFilterTypeIdentifier.id
|
||||
)
|
||||
|
||||
if (optionValueImagesAlreadyIncluded) {
|
||||
return accumulatedImageRecords
|
||||
}
|
||||
|
||||
includedOptionValuesImagesIds.push(
|
||||
spreeOptionValueOfFilterTypeIdentifier.id
|
||||
)
|
||||
|
||||
return [
|
||||
...accumulatedImageRecords,
|
||||
...jsonApi.findRelationshipDocuments(
|
||||
spreeSuccessResponse,
|
||||
spreeVariantRecord,
|
||||
'images'
|
||||
),
|
||||
]
|
||||
},
|
||||
[]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const spreeImageRecords = [
|
||||
...spreePrimaryVariantImageRecords,
|
||||
...spreeVariantImageRecords,
|
||||
]
|
||||
|
||||
const productImages = getMediaGallery(
|
||||
spreeImageRecords,
|
||||
createGetAbsoluteImageUrl(requireConfigValue('imageHost') as string)
|
||||
)
|
||||
|
||||
const images: ProductImage[] =
|
||||
productImages.length === 0
|
||||
? placeholderImage === false
|
||||
? []
|
||||
: [{ url: placeholderImage }]
|
||||
: productImages
|
||||
|
||||
const slug = spreeProduct.attributes.slug
|
||||
const path = getProductPath(spreeProduct)
|
||||
|
||||
return {
|
||||
id: spreeProduct.id,
|
||||
name: spreeProduct.attributes.name,
|
||||
description: spreeProduct.attributes.description,
|
||||
images,
|
||||
variants,
|
||||
options,
|
||||
price,
|
||||
slug,
|
||||
path,
|
||||
sku,
|
||||
}
|
||||
}
|
||||
|
||||
export default normalizeProduct
|
16
packages/spree/src/utils/normalizations/normalize-user.ts
Normal file
16
packages/spree/src/utils/normalizations/normalize-user.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Customer } from '@vercel/commerce/types/customer'
|
||||
import type { AccountAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Account'
|
||||
import type { SpreeSdkResponse } from '../../types'
|
||||
|
||||
const normalizeUser = (
|
||||
_spreeSuccessResponse: SpreeSdkResponse,
|
||||
spreeUser: AccountAttr
|
||||
): Customer => {
|
||||
const email = spreeUser.attributes.email
|
||||
|
||||
return {
|
||||
email,
|
||||
}
|
||||
}
|
||||
|
||||
export default normalizeUser
|
@@ -0,0 +1,68 @@
|
||||
import MissingProductError from '../../errors/MissingProductError'
|
||||
import MissingVariantError from '../../errors/MissingVariantError'
|
||||
import { jsonApi } from '@spree/storefront-api-v2-sdk'
|
||||
import type { ProductAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
||||
import type { WishedItemAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/WishedItem'
|
||||
import type { WishlistAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Wishlist'
|
||||
import type {
|
||||
ExplicitCommerceWishlist,
|
||||
SpreeSdkResponse,
|
||||
VariantAttr,
|
||||
} from '../../types'
|
||||
import normalizeProduct from './normalize-product'
|
||||
|
||||
const normalizeWishlist = (
|
||||
spreeSuccessResponse: SpreeSdkResponse,
|
||||
spreeWishlist: WishlistAttr
|
||||
): ExplicitCommerceWishlist => {
|
||||
const spreeWishedItems = jsonApi.findRelationshipDocuments<WishedItemAttr>(
|
||||
spreeSuccessResponse,
|
||||
spreeWishlist,
|
||||
'wished_items'
|
||||
)
|
||||
|
||||
const items: ExplicitCommerceWishlist['items'] = spreeWishedItems.map(
|
||||
(spreeWishedItem) => {
|
||||
const spreeWishedVariant =
|
||||
jsonApi.findSingleRelationshipDocument<VariantAttr>(
|
||||
spreeSuccessResponse,
|
||||
spreeWishedItem,
|
||||
'variant'
|
||||
)
|
||||
|
||||
if (spreeWishedVariant === null) {
|
||||
throw new MissingVariantError(
|
||||
`Couldn't find variant for wished item with id ${spreeWishedItem.id}.`
|
||||
)
|
||||
}
|
||||
|
||||
const spreeWishedProduct =
|
||||
jsonApi.findSingleRelationshipDocument<ProductAttr>(
|
||||
spreeSuccessResponse,
|
||||
spreeWishedVariant,
|
||||
'product'
|
||||
)
|
||||
|
||||
if (spreeWishedProduct === null) {
|
||||
throw new MissingProductError(
|
||||
`Couldn't find product for variant with id ${spreeWishedVariant.id}.`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
id: spreeWishedItem.id,
|
||||
product_id: parseInt(spreeWishedProduct.id, 10),
|
||||
variant_id: parseInt(spreeWishedVariant.id, 10),
|
||||
product: normalizeProduct(spreeSuccessResponse, spreeWishedProduct),
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
id: spreeWishlist.id,
|
||||
token: spreeWishlist.attributes.token,
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
||||
export default normalizeWishlist
|
21
packages/spree/src/utils/pretty-print-spree-sdk-errors.ts
Normal file
21
packages/spree/src/utils/pretty-print-spree-sdk-errors.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { errors } from '@spree/storefront-api-v2-sdk'
|
||||
|
||||
const prettyPrintSpreeSdkErrors = (error: errors.SpreeSDKError): string => {
|
||||
let prettyOutput = `Name: ${error.name}\nMessage: ${error.message}`
|
||||
|
||||
if (error instanceof errors.BasicSpreeError) {
|
||||
prettyOutput += `\nSpree summary: ${error.summary}`
|
||||
|
||||
if (error instanceof errors.ExpandedSpreeError) {
|
||||
prettyOutput += `\nSpree validation errors:\n${JSON.stringify(
|
||||
error.errors,
|
||||
null,
|
||||
2
|
||||
)}`
|
||||
}
|
||||
}
|
||||
|
||||
return prettyOutput
|
||||
}
|
||||
|
||||
export default prettyPrintSpreeSdkErrors
|
16
packages/spree/src/utils/require-config.ts
Normal file
16
packages/spree/src/utils/require-config.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import MissingConfigurationValueError from '../errors/MissingConfigurationValueError'
|
||||
import type { NonUndefined, ValueOf } from '../types'
|
||||
|
||||
const requireConfig = <T>(isomorphicConfig: T, key: keyof T) => {
|
||||
const valueUnderKey = isomorphicConfig[key]
|
||||
|
||||
if (typeof valueUnderKey === 'undefined') {
|
||||
throw new MissingConfigurationValueError(
|
||||
`Value for configuration key ${key} was undefined.`
|
||||
)
|
||||
}
|
||||
|
||||
return valueUnderKey as NonUndefined<ValueOf<T>>
|
||||
}
|
||||
|
||||
export default requireConfig
|
11
packages/spree/src/utils/sort-option-types.ts
Normal file
11
packages/spree/src/utils/sort-option-types.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { ExpandedProductOption } from '../types'
|
||||
|
||||
const sortOptionsByPosition = (
|
||||
options: ExpandedProductOption[]
|
||||
): ExpandedProductOption[] => {
|
||||
return options.sort((firstOption, secondOption) => {
|
||||
return firstOption.position - secondOption.position
|
||||
})
|
||||
}
|
||||
|
||||
export default sortOptionsByPosition
|
21
packages/spree/src/utils/tokens/cart-token.ts
Normal file
21
packages/spree/src/utils/tokens/cart-token.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { requireConfigValue } from '../../isomorphic-config'
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
export const getCartToken = () =>
|
||||
Cookies.get(requireConfigValue('cartCookieName') as string)
|
||||
|
||||
export const setCartToken = (cartToken: string) => {
|
||||
const cookieOptions = {
|
||||
expires: requireConfigValue('cartCookieExpire') as number,
|
||||
}
|
||||
|
||||
Cookies.set(
|
||||
requireConfigValue('cartCookieName') as string,
|
||||
cartToken,
|
||||
cookieOptions
|
||||
)
|
||||
}
|
||||
|
||||
export const removeCartToken = () => {
|
||||
Cookies.remove(requireConfigValue('cartCookieName') as string)
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
import { SpreeSdkResponseWithRawResponse } from '../../types'
|
||||
import type { Client } from '@spree/storefront-api-v2-sdk'
|
||||
import type { IOAuthToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import getSpreeSdkMethodFromEndpointPath from '../get-spree-sdk-method-from-endpoint-path'
|
||||
import {
|
||||
ensureUserTokenResponse,
|
||||
removeUserTokenResponse,
|
||||
setUserTokenResponse,
|
||||
} from './user-token-response'
|
||||
import AccessTokenError from '../../errors/AccessTokenError'
|
||||
|
||||
/**
|
||||
* If the user has a saved access token, make sure it's not expired
|
||||
* If it is expired, attempt to refresh it.
|
||||
*/
|
||||
const ensureFreshUserAccessToken = async (client: Client): Promise<void> => {
|
||||
const userTokenResponse = ensureUserTokenResponse()
|
||||
|
||||
if (!userTokenResponse) {
|
||||
// There's no user token or it has an invalid format.
|
||||
return
|
||||
}
|
||||
|
||||
const isAccessTokenExpired =
|
||||
(userTokenResponse.created_at + userTokenResponse.expires_in) * 1000 <
|
||||
Date.now()
|
||||
|
||||
if (!isAccessTokenExpired) {
|
||||
return
|
||||
}
|
||||
|
||||
const spreeRefreshAccessTokenSdkMethod = getSpreeSdkMethodFromEndpointPath<
|
||||
Client,
|
||||
SpreeSdkResponseWithRawResponse & IOAuthToken
|
||||
>(client, 'authentication.refreshToken')
|
||||
|
||||
const spreeRefreshAccessTokenResponse =
|
||||
await spreeRefreshAccessTokenSdkMethod({
|
||||
refresh_token: userTokenResponse.refresh_token,
|
||||
})
|
||||
|
||||
if (spreeRefreshAccessTokenResponse.isFail()) {
|
||||
removeUserTokenResponse()
|
||||
|
||||
throw new AccessTokenError('Could not refresh access token.')
|
||||
}
|
||||
|
||||
setUserTokenResponse(spreeRefreshAccessTokenResponse.success())
|
||||
}
|
||||
|
||||
export default ensureFreshUserAccessToken
|
25
packages/spree/src/utils/tokens/ensure-itoken.ts
Normal file
25
packages/spree/src/utils/tokens/ensure-itoken.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import { getCartToken } from './cart-token'
|
||||
import { ensureUserTokenResponse } from './user-token-response'
|
||||
|
||||
const ensureIToken = (): IToken | undefined => {
|
||||
const userTokenResponse = ensureUserTokenResponse()
|
||||
|
||||
if (userTokenResponse) {
|
||||
return {
|
||||
bearerToken: userTokenResponse.access_token,
|
||||
}
|
||||
}
|
||||
|
||||
const cartToken = getCartToken()
|
||||
|
||||
if (cartToken) {
|
||||
return {
|
||||
orderToken: cartToken,
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export default ensureIToken
|
9
packages/spree/src/utils/tokens/is-logged-in.ts
Normal file
9
packages/spree/src/utils/tokens/is-logged-in.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ensureUserTokenResponse } from './user-token-response'
|
||||
|
||||
const isLoggedIn = (): boolean => {
|
||||
const userTokenResponse = ensureUserTokenResponse()
|
||||
|
||||
return !!userTokenResponse
|
||||
}
|
||||
|
||||
export default isLoggedIn
|
49
packages/spree/src/utils/tokens/revoke-user-tokens.ts
Normal file
49
packages/spree/src/utils/tokens/revoke-user-tokens.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import type { HookFetcherContext } from '@vercel/commerce/utils/types'
|
||||
import TokensNotRejectedError from '../../errors/TokensNotRejectedError'
|
||||
import type { UserOAuthTokens } from '../../types'
|
||||
import type { EmptyObjectResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/EmptyObject'
|
||||
|
||||
const revokeUserTokens = async (
|
||||
fetch: HookFetcherContext<{
|
||||
data: any
|
||||
}>['fetch'],
|
||||
userTokens: UserOAuthTokens
|
||||
): Promise<void> => {
|
||||
const spreeRevokeTokensResponses = await Promise.allSettled([
|
||||
fetch<GraphQLFetcherResult<EmptyObjectResponse>>({
|
||||
variables: {
|
||||
methodPath: 'authentication.revokeToken',
|
||||
arguments: [
|
||||
{
|
||||
token: userTokens.refreshToken,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
fetch<GraphQLFetcherResult<EmptyObjectResponse>>({
|
||||
variables: {
|
||||
methodPath: 'authentication.revokeToken',
|
||||
arguments: [
|
||||
{
|
||||
token: userTokens.accessToken,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
])
|
||||
|
||||
const anyRejected = spreeRevokeTokensResponses.some(
|
||||
(response) => response.status === 'rejected'
|
||||
)
|
||||
|
||||
if (anyRejected) {
|
||||
throw new TokensNotRejectedError(
|
||||
'Some tokens could not be rejected in Spree.'
|
||||
)
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
export default revokeUserTokens
|
58
packages/spree/src/utils/tokens/user-token-response.ts
Normal file
58
packages/spree/src/utils/tokens/user-token-response.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { requireConfigValue } from '../../isomorphic-config'
|
||||
import Cookies from 'js-cookie'
|
||||
import type { IOAuthToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import UserTokenResponseParseError from '../../errors/UserTokenResponseParseError'
|
||||
|
||||
export const getUserTokenResponse = (): IOAuthToken | undefined => {
|
||||
const stringifiedToken = Cookies.get(
|
||||
requireConfigValue('userCookieName') as string
|
||||
)
|
||||
|
||||
if (!stringifiedToken) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const token: IOAuthToken = JSON.parse(stringifiedToken)
|
||||
|
||||
return token
|
||||
} catch (parseError) {
|
||||
throw new UserTokenResponseParseError(
|
||||
'Could not parse stored user token response.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the saved user token response. If the response fails json parsing,
|
||||
* removes the saved token and returns @type {undefined} instead.
|
||||
*/
|
||||
export const ensureUserTokenResponse = (): IOAuthToken | undefined => {
|
||||
try {
|
||||
return getUserTokenResponse()
|
||||
} catch (error) {
|
||||
if (error instanceof UserTokenResponseParseError) {
|
||||
removeUserTokenResponse()
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export const setUserTokenResponse = (token: IOAuthToken) => {
|
||||
const cookieOptions = {
|
||||
expires: requireConfigValue('userCookieExpire') as number,
|
||||
}
|
||||
|
||||
Cookies.set(
|
||||
requireConfigValue('userCookieName') as string,
|
||||
JSON.stringify(token),
|
||||
cookieOptions
|
||||
)
|
||||
}
|
||||
|
||||
export const removeUserTokenResponse = () => {
|
||||
Cookies.remove(requireConfigValue('userCookieName') as string)
|
||||
}
|
@@ -0,0 +1,13 @@
|
||||
const validateAllProductsTaxonomyId = (taxonomyId: unknown): string | false => {
|
||||
if (!taxonomyId || taxonomyId === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof taxonomyId === 'string') {
|
||||
return taxonomyId
|
||||
}
|
||||
|
||||
throw new TypeError('taxonomyId must be a string or falsy.')
|
||||
}
|
||||
|
||||
export default validateAllProductsTaxonomyId
|
@@ -0,0 +1,21 @@
|
||||
const validateCookieExpire = (expire: unknown): number => {
|
||||
let expireInteger: number
|
||||
|
||||
if (typeof expire === 'string') {
|
||||
expireInteger = parseFloat(expire)
|
||||
} else if (typeof expire === 'number') {
|
||||
expireInteger = expire
|
||||
} else {
|
||||
throw new TypeError(
|
||||
'expire must be a string containing a number or an integer.'
|
||||
)
|
||||
}
|
||||
|
||||
if (expireInteger < 0) {
|
||||
throw new RangeError('expire must be non-negative.')
|
||||
}
|
||||
|
||||
return expireInteger
|
||||
}
|
||||
|
||||
export default validateCookieExpire
|
@@ -0,0 +1,15 @@
|
||||
const validateImagesOptionFilter = (
|
||||
optionTypeNameOrFalse: unknown
|
||||
): string | false => {
|
||||
if (!optionTypeNameOrFalse || optionTypeNameOrFalse === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof optionTypeNameOrFalse === 'string') {
|
||||
return optionTypeNameOrFalse
|
||||
}
|
||||
|
||||
throw new TypeError('optionTypeNameOrFalse must be a string or falsy.')
|
||||
}
|
||||
|
||||
export default validateImagesOptionFilter
|
@@ -0,0 +1,23 @@
|
||||
const validateImagesQuality = (quality: unknown): number => {
|
||||
let quality_level: number
|
||||
|
||||
if (typeof quality === 'string') {
|
||||
quality_level = parseInt(quality)
|
||||
} else if (typeof quality === 'number') {
|
||||
quality_level = quality
|
||||
} else {
|
||||
throw new TypeError(
|
||||
'prerenderCount count must be a string containing a number or an integer.'
|
||||
)
|
||||
}
|
||||
|
||||
if (quality_level === NaN) {
|
||||
throw new TypeError(
|
||||
'prerenderCount count must be a string containing a number or an integer.'
|
||||
)
|
||||
}
|
||||
|
||||
return quality_level
|
||||
}
|
||||
|
||||
export default validateImagesQuality
|
13
packages/spree/src/utils/validations/validate-images-size.ts
Normal file
13
packages/spree/src/utils/validations/validate-images-size.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
const validateImagesSize = (size: unknown): string => {
|
||||
if (typeof size !== 'string') {
|
||||
throw new TypeError('size must be a string.')
|
||||
}
|
||||
|
||||
if (!size.includes('x') || size.split('x').length != 2) {
|
||||
throw new Error("size must have two numbers separated with an 'x'")
|
||||
}
|
||||
|
||||
return size
|
||||
}
|
||||
|
||||
export default validateImagesSize
|
@@ -0,0 +1,15 @@
|
||||
const validatePlaceholderImageUrl = (
|
||||
placeholderUrlOrFalse: unknown
|
||||
): string | false => {
|
||||
if (!placeholderUrlOrFalse || placeholderUrlOrFalse === 'false') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof placeholderUrlOrFalse === 'string') {
|
||||
return placeholderUrlOrFalse
|
||||
}
|
||||
|
||||
throw new TypeError('placeholderUrlOrFalse must be a string or falsy.')
|
||||
}
|
||||
|
||||
export default validatePlaceholderImageUrl
|
@@ -0,0 +1,21 @@
|
||||
const validateProductsPrerenderCount = (prerenderCount: unknown): number => {
|
||||
let prerenderCountInteger: number
|
||||
|
||||
if (typeof prerenderCount === 'string') {
|
||||
prerenderCountInteger = parseInt(prerenderCount)
|
||||
} else if (typeof prerenderCount === 'number') {
|
||||
prerenderCountInteger = prerenderCount
|
||||
} else {
|
||||
throw new TypeError(
|
||||
'prerenderCount count must be a string containing a number or an integer.'
|
||||
)
|
||||
}
|
||||
|
||||
if (prerenderCountInteger < 0) {
|
||||
throw new RangeError('prerenderCount must be non-negative.')
|
||||
}
|
||||
|
||||
return prerenderCountInteger
|
||||
}
|
||||
|
||||
export default validateProductsPrerenderCount
|
3
packages/spree/src/wishlist/index.ts
Normal file
3
packages/spree/src/wishlist/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as useAddItem } from './use-add-item'
|
||||
export { default as useWishlist } from './use-wishlist'
|
||||
export { default as useRemoveItem } from './use-remove-item'
|
88
packages/spree/src/wishlist/use-add-item.tsx
Normal file
88
packages/spree/src/wishlist/use-add-item.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useAddItem from '@vercel/commerce/wishlist/use-add-item'
|
||||
import type { UseAddItem } from '@vercel/commerce/wishlist/use-add-item'
|
||||
import useWishlist from './use-wishlist'
|
||||
import type { ExplicitWishlistAddItemHook } from '../types'
|
||||
import type {
|
||||
WishedItem,
|
||||
WishlistsAddWishedItem,
|
||||
} from '@spree/storefront-api-v2-sdk/types/interfaces/WishedItem'
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import ensureIToken from '../utils/tokens/ensure-itoken'
|
||||
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import type { AddItemHook } from '@vercel/commerce/types/wishlist'
|
||||
import isLoggedIn from '../utils/tokens/is-logged-in'
|
||||
|
||||
export default useAddItem as UseAddItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<ExplicitWishlistAddItemHook> = {
|
||||
fetchOptions: {
|
||||
url: 'wishlists',
|
||||
query: 'addWishedItem',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
console.info(
|
||||
'useAddItem (wishlist) fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
const {
|
||||
item: { productId, variantId, wishlistToken },
|
||||
} = input
|
||||
|
||||
if (!isLoggedIn() || !wishlistToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
let token: IToken | undefined = ensureIToken()
|
||||
|
||||
const addItemParameters: WishlistsAddWishedItem = {
|
||||
variant_id: `${variantId}`,
|
||||
quantity: 1,
|
||||
}
|
||||
|
||||
await fetch<GraphQLFetcherResult<WishedItem>>({
|
||||
variables: {
|
||||
methodPath: 'wishlists.addWishedItem',
|
||||
arguments: [token, wishlistToken, addItemParameters],
|
||||
},
|
||||
})
|
||||
|
||||
return null
|
||||
},
|
||||
useHook: ({ fetch }) => {
|
||||
const useWrappedHook: ReturnType<
|
||||
MutationHook<AddItemHook>['useHook']
|
||||
> = () => {
|
||||
const wishlist = useWishlist()
|
||||
|
||||
return useCallback(
|
||||
async (item) => {
|
||||
if (!wishlist.data) {
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await fetch({
|
||||
input: {
|
||||
item: {
|
||||
...item,
|
||||
wishlistToken: wishlist.data.token,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await wishlist.mutate()
|
||||
|
||||
return data
|
||||
},
|
||||
[wishlist]
|
||||
)
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
75
packages/spree/src/wishlist/use-remove-item.tsx
Normal file
75
packages/spree/src/wishlist/use-remove-item.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { useCallback } from 'react'
|
||||
import type { MutationHook } from '@vercel/commerce/utils/types'
|
||||
import useRemoveItem from '@vercel/commerce/wishlist/use-remove-item'
|
||||
import type { UseRemoveItem } from '@vercel/commerce/wishlist/use-remove-item'
|
||||
import useWishlist from './use-wishlist'
|
||||
import type { ExplicitWishlistRemoveItemHook } from '../types'
|
||||
import isLoggedIn from '../utils/tokens/is-logged-in'
|
||||
import ensureIToken from '../utils/tokens/ensure-itoken'
|
||||
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import type { WishedItem } from '@spree/storefront-api-v2-sdk/types/interfaces/WishedItem'
|
||||
|
||||
export default useRemoveItem as UseRemoveItem<typeof handler>
|
||||
|
||||
export const handler: MutationHook<ExplicitWishlistRemoveItemHook> = {
|
||||
fetchOptions: {
|
||||
url: 'wishlists',
|
||||
query: 'removeWishedItem',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
console.info(
|
||||
'useRemoveItem (wishlist) fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
const { itemId, wishlistToken } = input
|
||||
|
||||
if (!isLoggedIn() || !wishlistToken) {
|
||||
return null
|
||||
}
|
||||
|
||||
let token: IToken | undefined = ensureIToken()
|
||||
|
||||
await fetch<GraphQLFetcherResult<WishedItem>>({
|
||||
variables: {
|
||||
methodPath: 'wishlists.removeWishedItem',
|
||||
arguments: [token, wishlistToken, itemId],
|
||||
},
|
||||
})
|
||||
|
||||
return null
|
||||
},
|
||||
useHook: ({ fetch }) => {
|
||||
const useWrappedHook: ReturnType<
|
||||
MutationHook<ExplicitWishlistRemoveItemHook>['useHook']
|
||||
> = () => {
|
||||
const wishlist = useWishlist()
|
||||
|
||||
return useCallback(
|
||||
async (input) => {
|
||||
if (!wishlist.data) {
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await fetch({
|
||||
input: {
|
||||
itemId: `${input.id}`,
|
||||
wishlistToken: wishlist.data.token,
|
||||
},
|
||||
})
|
||||
|
||||
await wishlist.mutate()
|
||||
|
||||
return data
|
||||
},
|
||||
[wishlist]
|
||||
)
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
93
packages/spree/src/wishlist/use-wishlist.tsx
Normal file
93
packages/spree/src/wishlist/use-wishlist.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useMemo } from 'react'
|
||||
import type { SWRHook } from '@vercel/commerce/utils/types'
|
||||
import useWishlist from '@vercel/commerce/wishlist/use-wishlist'
|
||||
import type { UseWishlist } from '@vercel/commerce/wishlist/use-wishlist'
|
||||
import type { GetWishlistHook } from '@vercel/commerce/types/wishlist'
|
||||
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||
import type { GraphQLFetcherResult } from '@vercel/commerce/api'
|
||||
import type { Wishlist } from '@spree/storefront-api-v2-sdk/types/interfaces/Wishlist'
|
||||
import ensureIToken from '../utils/tokens/ensure-itoken'
|
||||
import normalizeWishlist from '../utils/normalizations/normalize-wishlist'
|
||||
import isLoggedIn from '../utils/tokens/is-logged-in'
|
||||
|
||||
export default useWishlist as UseWishlist<typeof handler>
|
||||
|
||||
export const handler: SWRHook<GetWishlistHook> = {
|
||||
// Provide fetchOptions for SWR cache key
|
||||
fetchOptions: {
|
||||
url: 'wishlists',
|
||||
query: 'default',
|
||||
},
|
||||
async fetcher({ input, options, fetch }) {
|
||||
console.info(
|
||||
'useWishlist fetcher called. Configuration: ',
|
||||
'input: ',
|
||||
input,
|
||||
'options: ',
|
||||
options
|
||||
)
|
||||
|
||||
if (!isLoggedIn()) {
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: Optimize with includeProducts.
|
||||
|
||||
const token: IToken | undefined = ensureIToken()
|
||||
|
||||
const { data: spreeWishlistsDefaultSuccessResponse } = await fetch<
|
||||
GraphQLFetcherResult<Wishlist>
|
||||
>({
|
||||
variables: {
|
||||
methodPath: 'wishlists.default',
|
||||
arguments: [
|
||||
token,
|
||||
{
|
||||
include: [
|
||||
'wished_items',
|
||||
'wished_items.variant',
|
||||
'wished_items.variant.product',
|
||||
'wished_items.variant.product.primary_variant',
|
||||
'wished_items.variant.product.images',
|
||||
'wished_items.variant.product.option_types',
|
||||
'wished_items.variant.product.variants',
|
||||
'wished_items.variant.product.variants.option_values',
|
||||
].join(','),
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
return normalizeWishlist(
|
||||
spreeWishlistsDefaultSuccessResponse,
|
||||
spreeWishlistsDefaultSuccessResponse.data
|
||||
)
|
||||
},
|
||||
useHook: ({ useData }) => {
|
||||
const useWrappedHook: ReturnType<SWRHook<GetWishlistHook>['useHook']> = (
|
||||
input
|
||||
) => {
|
||||
const response = useData({
|
||||
swrOptions: {
|
||||
revalidateOnFocus: false,
|
||||
...input?.swrOptions,
|
||||
},
|
||||
})
|
||||
|
||||
return useMemo(
|
||||
() =>
|
||||
Object.create(response, {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.items?.length || 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
}),
|
||||
[response]
|
||||
)
|
||||
}
|
||||
|
||||
return useWrappedHook
|
||||
},
|
||||
}
|
Reference in New Issue
Block a user