mirror of
https://github.com/vercel/commerce.git
synced 2025-06-17 20:51:21 +00:00
add spree module
This commit is contained in:
parent
42d5d8efcf
commit
c625e1cc89
BIN
lib/spree/README-assets/screenshots.png
Normal file
BIN
lib/spree/README-assets/screenshots.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 114 KiB |
33
lib/spree/README.md
Normal file
33
lib/spree/README.md
Normal file
@ -0,0 +1,33 @@
|
||||
# [Spree Commerce][1] Provider
|
||||
|
||||
![Screenshots of Spree Commerce and NextJS Commerce][5]
|
||||
|
||||
An integration of [Spree Commerce](https://spreecommerce.org/) within NextJS Commerce. It supports browsing and searching Spree products and adding products to the cart.
|
||||
|
||||
**Demo**: [https://spree.vercel.store/][6]
|
||||
|
||||
## Installation
|
||||
|
||||
1. Setup Spree - [follow the Getting Started guide](https://dev-docs.spreecommerce.org/getting-started/installation).
|
||||
|
||||
1. Setup Nextjs Commerce - [instructions for setting up NextJS Commerce][2].
|
||||
|
||||
1. Copy the `.env.template` file in this directory (`/framework/spree`) to `.env.local` in the main directory
|
||||
|
||||
```bash
|
||||
cp framework/spree/.env.template .env.local
|
||||
```
|
||||
|
||||
1. Set `NEXT_PUBLIC_SPREE_CATEGORIES_TAXONOMY_PERMALINK` and `NEXT_PUBLIC_SPREE_BRANDS_TAXONOMY_PERMALINK` environment variables:
|
||||
|
||||
- They rely on [taxonomies'](https://dev-docs.spreecommerce.org/internals/products#taxons-and-taxonomies) permalinks in Spree.
|
||||
- Go to the Spree admin panel and create `Categories` and `Brands` taxonomies if they don't exist and copy their permalinks into `.env.local` in NextJS Commerce.
|
||||
|
||||
1. Finally, run `npm run dev` :tada:
|
||||
|
||||
[1]: https://spreecommerce.org/
|
||||
[2]: https://github.com/vercel/commerce
|
||||
[3]: https://github.com/spree/spree_starter
|
||||
[4]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
||||
[5]: ./README-assets/screenshots.png
|
||||
[6]: https://spree.vercel.store/
|
1
lib/spree/api/endpoints/cart/index.ts
Normal file
1
lib/spree/api/endpoints/cart/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
lib/spree/api/endpoints/catalog/index.ts
Normal file
1
lib/spree/api/endpoints/catalog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
lib/spree/api/endpoints/catalog/products.ts
Normal file
1
lib/spree/api/endpoints/catalog/products.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
44
lib/spree/api/endpoints/checkout/get-checkout.ts
Normal file
44
lib/spree/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;
|
19
lib/spree/api/endpoints/checkout/index.ts
Normal file
19
lib/spree/api/endpoints/checkout/index.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { createEndpoint } from '@commerce/api';
|
||||
import type { GetAPISchema, CommerceAPI } from '@commerce/api';
|
||||
import checkoutEndpoint from '@commerce/api/endpoints/checkout';
|
||||
import type { CheckoutSchema } from '@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
lib/spree/api/endpoints/customer/address.ts
Normal file
1
lib/spree/api/endpoints/customer/address.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
lib/spree/api/endpoints/customer/card.ts
Normal file
1
lib/spree/api/endpoints/customer/card.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
lib/spree/api/endpoints/customer/index.ts
Normal file
1
lib/spree/api/endpoints/customer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
lib/spree/api/endpoints/login/index.ts
Normal file
1
lib/spree/api/endpoints/login/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
lib/spree/api/endpoints/logout/index.ts
Normal file
1
lib/spree/api/endpoints/logout/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
lib/spree/api/endpoints/signup/index.ts
Normal file
1
lib/spree/api/endpoints/signup/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
lib/spree/api/endpoints/wishlist/index.tsx
Normal file
1
lib/spree/api/endpoints/wishlist/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
48
lib/spree/api/index.ts
Normal file
48
lib/spree/api/index.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import type { CommerceAPI, CommerceAPIConfig } from '@commerce/api';
|
||||
import { getCommerceApi as commerceApi } from '@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';
|
||||
import getAllTaxons from './operations/get-all-taxons';
|
||||
import getProducts from './operations/get-products';
|
||||
|
||||
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,
|
||||
getAllTaxons,
|
||||
getProducts
|
||||
};
|
||||
|
||||
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);
|
||||
}
|
72
lib/spree/api/operations/get-all-pages.ts
Normal file
72
lib/spree/api/operations/get-all-pages.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import type { OperationContext, OperationOptions } from '@commerce/api/operations';
|
||||
import type { GetAllPagesOperation, Page } from '@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;
|
||||
}
|
91
lib/spree/api/operations/get-all-product-paths.ts
Normal file
91
lib/spree/api/operations/get-all-product-paths.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import type { OperationContext, OperationOptions } from '@commerce/api/operations';
|
||||
import type { Product } from '@commerce/types/product';
|
||||
import type { GetAllProductPathsOperation } from '@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;
|
||||
}
|
84
lib/spree/api/operations/get-all-products.ts
Normal file
84
lib/spree/api/operations/get-all-products.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import type { Product } from '@commerce/types/product';
|
||||
import type { GetAllProductsOperation } from '@commerce/types/product';
|
||||
import type { OperationContext, OperationOptions } from '@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;
|
||||
}
|
63
lib/spree/api/operations/get-all-taxons.ts
Normal file
63
lib/spree/api/operations/get-all-taxons.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import _ from 'lodash';
|
||||
import type { ITaxons, TaxonAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Taxon';
|
||||
import type { SpreeSdkVariables } from '../../types';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const buildTaxonsTree = (taxons: TaxonAttr[], parentId: string | number): TaxonAttr[] => {
|
||||
const children = _.chain(taxons)
|
||||
.filter((item) => {
|
||||
const relationships = item.relationships || {};
|
||||
return parentId === _.get(relationships, 'parent.data.id');
|
||||
})
|
||||
.sort(taxonsSort)
|
||||
.value();
|
||||
|
||||
return children.map((child) => ({
|
||||
id: child.id,
|
||||
name: child.attributes.name,
|
||||
type: child.type,
|
||||
position: child.attributes.position,
|
||||
children: buildTaxonsTree(taxons, child.id)
|
||||
}));
|
||||
};
|
||||
|
||||
export default function getAllTaxonsOperation({ commerce, locale }) {
|
||||
async function getAllTaxons(options = {}) {
|
||||
const { config: userConfig } = options;
|
||||
|
||||
const config = commerce.getConfig(userConfig);
|
||||
|
||||
const { fetch: apiFetch } = config;
|
||||
|
||||
const variables: SpreeSdkVariables = {
|
||||
methodPath: 'taxons.list',
|
||||
arguments: [
|
||||
{
|
||||
locale: config.locale
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const { data: spreeSuccessResponse } = await apiFetch('__UNUSED__', { variables });
|
||||
|
||||
const normalizedTaxons = buildTaxonsTree(spreeSuccessResponse.data, '1');
|
||||
|
||||
return { taxons: normalizedTaxons };
|
||||
}
|
||||
|
||||
return getAllTaxons;
|
||||
}
|
6
lib/spree/api/operations/get-customer-wishlist.ts
Normal file
6
lib/spree/api/operations/get-customer-wishlist.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export default function getCustomerWishlistOperation() {
|
||||
function getCustomerWishlist(): any {
|
||||
return { wishlist: {} };
|
||||
}
|
||||
return getCustomerWishlist;
|
||||
}
|
73
lib/spree/api/operations/get-page.ts
Normal file
73
lib/spree/api/operations/get-page.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import type { OperationContext, OperationOptions } from '@commerce/api/operations';
|
||||
import type { GetPageOperation } from '@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;
|
||||
}
|
81
lib/spree/api/operations/get-product.ts
Normal file
81
lib/spree/api/operations/get-product.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import type { SpreeApiConfig, SpreeApiProvider } from '../index';
|
||||
import type { GetProductOperation } from '@commerce/types/product';
|
||||
import type { OperationContext, OperationOptions } from '@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;
|
||||
}
|
37
lib/spree/api/operations/get-products.js
Normal file
37
lib/spree/api/operations/get-products.js
Normal file
@ -0,0 +1,37 @@
|
||||
import normalizeProduct from '../../utils/normalizations/normalize-product';
|
||||
import { requireConfigValue } from '../../isomorphic-config';
|
||||
|
||||
const imagesSize = requireConfigValue('imagesSize');
|
||||
const imagesQuality = requireConfigValue('imagesQuality');
|
||||
|
||||
export default function getProductsOperation({ commerce }) {
|
||||
async function getProducts({ taxons = [], config: userConfig } = {}) {
|
||||
const filter = { filter: { taxons: taxons.join(',') }, sort: '-updated_at' };
|
||||
|
||||
const variables = {
|
||||
methodPath: 'products.list',
|
||||
arguments: [
|
||||
{
|
||||
include: 'primary_variant,variants,images,option_types,variants.option_values',
|
||||
per_page: 100,
|
||||
...filter,
|
||||
image_transformation: {
|
||||
quality: imagesQuality,
|
||||
size: imagesSize
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const config = commerce.getConfig(userConfig);
|
||||
const { fetch: apiFetch } = config;
|
||||
const { data: spreeSuccessResponse } = await apiFetch('__UNUSED__', { variables });
|
||||
const normalizedProducts = spreeSuccessResponse.data.map((spreeProduct) =>
|
||||
normalizeProduct(spreeSuccessResponse, spreeProduct)
|
||||
);
|
||||
|
||||
return { products: normalizedProducts };
|
||||
}
|
||||
|
||||
return getProducts;
|
||||
}
|
120
lib/spree/api/operations/get-site-info.ts
Normal file
120
lib/spree/api/operations/get-site-info.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import type { OperationContext, OperationOptions } from '@commerce/api/operations';
|
||||
import type { Category, GetSiteInfoOperation } from '@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;
|
||||
}
|
8
lib/spree/api/operations/index.ts
Normal file
8
lib/spree/api/operations/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
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';
|
||||
export { default as getAllTaxons } from './get-all-taxons';
|
||||
export { default as getProducts } from './get-products';
|
74
lib/spree/api/utils/create-api-fetch.ts
Normal file
74
lib/spree/api/utils/create-api-fetch.ts
Normal file
@ -0,0 +1,74 @@
|
||||
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 '@commerce/api';
|
||||
import createCustomizedFetchFetcher, {
|
||||
fetchResponseKey
|
||||
} from '../../utils/create-customized-fetch-fetcher';
|
||||
import fetch, { Request } from 'node-fetch';
|
||||
import type { SpreeSdkResponseWithRawResponse } from '../../types';
|
||||
|
||||
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) {
|
||||
throw convertSpreeErrorToGraphQlError(storeResponseError);
|
||||
}
|
||||
|
||||
throw storeResponseError;
|
||||
};
|
||||
};
|
||||
|
||||
export default createApiFetch;
|
3
lib/spree/api/utils/fetch.ts
Normal file
3
lib/spree/api/utils/fetch.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import vercelFetch from '@vercel/fetch';
|
||||
|
||||
export default vercelFetch();
|
3
lib/spree/auth/index.ts
Normal file
3
lib/spree/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';
|
81
lib/spree/auth/use-login.tsx
Normal file
81
lib/spree/auth/use-login.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { MutationHook } from '@commerce/utils/types';
|
||||
import useLogin, { UseLogin } from '@commerce/auth/use-login';
|
||||
import type { LoginHook } from '@commerce/types/login';
|
||||
import type { AuthTokenAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Authentication';
|
||||
import { FetcherError, ValidationError } from '@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.revalidate();
|
||||
await cart.revalidate();
|
||||
await wishlist.revalidate();
|
||||
|
||||
return data;
|
||||
},
|
||||
[customer, cart, wishlist]
|
||||
);
|
||||
};
|
||||
|
||||
return useWrappedHook;
|
||||
}
|
||||
};
|
79
lib/spree/auth/use-logout.tsx
Normal file
79
lib/spree/auth/use-logout.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import { MutationHook } from '@commerce/utils/types';
|
||||
import useLogout, { UseLogout } from '@commerce/auth/use-logout';
|
||||
import type { LogoutHook } from '@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;
|
||||
}
|
||||
};
|
94
lib/spree/auth/use-signup.tsx
Normal file
94
lib/spree/auth/use-signup.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { GraphQLFetcherResult } from '@commerce/api';
|
||||
import type { MutationHook } from '@commerce/utils/types';
|
||||
import useSignup, { UseSignup } from '@commerce/auth/use-signup';
|
||||
import type { SignupHook } from '@commerce/types/signup';
|
||||
import { ValidationError } from '@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.revalidate();
|
||||
await cart.revalidate();
|
||||
await wishlist.revalidate();
|
||||
|
||||
return data;
|
||||
},
|
||||
[customer, cart, wishlist]
|
||||
);
|
||||
};
|
||||
|
||||
return useWrappedHook;
|
||||
}
|
||||
};
|
4
lib/spree/cart/index.ts
Normal file
4
lib/spree/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';
|
109
lib/spree/cart/use-add-item.tsx
Normal file
109
lib/spree/cart/use-add-item.tsx
Normal file
@ -0,0 +1,109 @@
|
||||
import useAddItem from '@commerce/cart/use-add-item';
|
||||
import type { UseAddItem } from '@commerce/cart/use-add-item';
|
||||
import type { MutationHook } from '@commerce/utils/types';
|
||||
import { useCallback } from 'react';
|
||||
import useCart from './use-cart';
|
||||
import type { AddItemHook } from '@commerce/types/cart';
|
||||
import normalizeCart from '../utils/normalizations/normalize-cart';
|
||||
import type { GraphQLFetcherResult } from '@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 '@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;
|
||||
}
|
||||
};
|
108
lib/spree/cart/use-cart.tsx
Normal file
108
lib/spree/cart/use-cart.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { SWRHook } from '@commerce/utils/types';
|
||||
import useCart from '@commerce/cart/use-cart';
|
||||
import type { UseCart } from '@commerce/cart/use-cart';
|
||||
import type { GetCartHook } from '@commerce/types/cart';
|
||||
import normalizeCart from '../utils/normalizations/normalize-cart';
|
||||
import type { GraphQLFetcherResult } from '@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 '@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 @framework/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;
|
||||
}
|
||||
};
|
107
lib/spree/cart/use-remove-item.tsx
Normal file
107
lib/spree/cart/use-remove-item.tsx
Normal file
@ -0,0 +1,107 @@
|
||||
import type { MutationHook } from '@commerce/utils/types';
|
||||
import useRemoveItem from '@commerce/cart/use-remove-item';
|
||||
import type { UseRemoveItem } from '@commerce/cart/use-remove-item';
|
||||
import type { RemoveItemHook } from '@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 '@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 '@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;
|
||||
}
|
||||
};
|
134
lib/spree/cart/use-update-item.tsx
Normal file
134
lib/spree/cart/use-update-item.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import type { MutationHook } from '@commerce/utils/types';
|
||||
import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item';
|
||||
import type { UpdateItemHook } from '@commerce/types/cart';
|
||||
import useCart from './use-cart';
|
||||
import { useMemo } from 'react';
|
||||
import { FetcherError, ValidationError } from '@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 '@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;
|
||||
}
|
||||
};
|
17
lib/spree/checkout/use-checkout.tsx
Normal file
17
lib/spree/checkout/use-checkout.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { SWRHook } from '@commerce/utils/types';
|
||||
import useCheckout, { UseCheckout } from '@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
lib/spree/commerce.config.json
Normal file
10
lib/spree/commerce.config.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"provider": "spree",
|
||||
"features": {
|
||||
"wishlist": true,
|
||||
"cart": true,
|
||||
"search": true,
|
||||
"customerAuth": true,
|
||||
"customCheckout": false
|
||||
}
|
||||
}
|
18
lib/spree/customer/address/use-add-item.tsx
Normal file
18
lib/spree/customer/address/use-add-item.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import useAddItem from '@commerce/customer/address/use-add-item';
|
||||
import type { UseAddItem } from '@commerce/customer/address/use-add-item';
|
||||
import type { MutationHook } from '@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
lib/spree/customer/card/use-add-item.tsx
Normal file
19
lib/spree/customer/card/use-add-item.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import useAddItem from '@commerce/customer/address/use-add-item';
|
||||
import type { UseAddItem } from '@commerce/customer/address/use-add-item';
|
||||
import type { MutationHook } from '@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
lib/spree/customer/index.ts
Normal file
1
lib/spree/customer/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as useCustomer } from './use-customer';
|
75
lib/spree/customer/use-customer.tsx
Normal file
75
lib/spree/customer/use-customer.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import type { SWRHook } from '@commerce/utils/types';
|
||||
import useCustomer from '@commerce/customer/use-customer';
|
||||
import type { UseCustomer } from '@commerce/customer/use-customer';
|
||||
import type { CustomerHook } from '@commerce/types/customer';
|
||||
import type { IToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token';
|
||||
import type { GraphQLFetcherResult } from '@commerce/api';
|
||||
import type { IAccount } from '@spree/storefront-api-v2-sdk/types/interfaces/Account';
|
||||
import { FetcherError } from '@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
lib/spree/errors/AccessTokenError.ts
Normal file
1
lib/spree/errors/AccessTokenError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class AccessTokenError extends Error {}
|
1
lib/spree/errors/MisconfigurationError.ts
Normal file
1
lib/spree/errors/MisconfigurationError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class MisconfigurationError extends Error {}
|
1
lib/spree/errors/MissingConfigurationValueError.ts
Normal file
1
lib/spree/errors/MissingConfigurationValueError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class MissingConfigurationValueError extends Error {}
|
1
lib/spree/errors/MissingLineItemVariantError.ts
Normal file
1
lib/spree/errors/MissingLineItemVariantError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class MissingLineItemVariantError extends Error {}
|
1
lib/spree/errors/MissingOptionValueError.ts
Normal file
1
lib/spree/errors/MissingOptionValueError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class MissingOptionValueError extends Error {}
|
1
lib/spree/errors/MissingPrimaryVariantError.ts
Normal file
1
lib/spree/errors/MissingPrimaryVariantError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class MissingPrimaryVariantError extends Error {}
|
1
lib/spree/errors/MissingProductError.ts
Normal file
1
lib/spree/errors/MissingProductError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class MissingProductError extends Error {}
|
1
lib/spree/errors/MissingSlugVariableError.ts
Normal file
1
lib/spree/errors/MissingSlugVariableError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class MissingSlugVariableError extends Error {}
|
1
lib/spree/errors/MissingVariantError.ts
Normal file
1
lib/spree/errors/MissingVariantError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class MissingVariantError extends Error {}
|
1
lib/spree/errors/RefreshTokenError.ts
Normal file
1
lib/spree/errors/RefreshTokenError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class RefreshTokenError extends Error {}
|
1
lib/spree/errors/SpreeResponseContentError.ts
Normal file
1
lib/spree/errors/SpreeResponseContentError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class SpreeResponseContentError extends Error {}
|
1
lib/spree/errors/SpreeSdkMethodFromEndpointPathError.ts
Normal file
1
lib/spree/errors/SpreeSdkMethodFromEndpointPathError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class SpreeSdkMethodFromEndpointPathError extends Error {}
|
1
lib/spree/errors/TokensNotRejectedError.ts
Normal file
1
lib/spree/errors/TokensNotRejectedError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class TokensNotRejectedError extends Error {}
|
1
lib/spree/errors/UserTokenResponseParseError.ts
Normal file
1
lib/spree/errors/UserTokenResponseParseError.ts
Normal file
@ -0,0 +1 @@
|
||||
export default class UserTokenResponseParseError extends Error {}
|
9
lib/spree/index.tsx
Normal file
9
lib/spree/index.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
const createAxiosFetcher = require('@spree/axios-fetcher/dist/server/index').default;
|
||||
import { makeClient } from '@spree/storefront-api-v2-sdk/dist/client';
|
||||
|
||||
const spreeClient = makeClient({
|
||||
host: 'http://localhost:3000',
|
||||
createFetcher: createAxiosFetcher
|
||||
});
|
||||
|
||||
export default spreeClient;
|
16
lib/spree/next.config.js
Normal file
16
lib/spree/next.config.js
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
lib/spree/product/index.ts
Normal file
2
lib/spree/product/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as usePrice } from './use-price';
|
||||
export { default as useSearch } from './use-search';
|
2
lib/spree/product/use-price.tsx
Normal file
2
lib/spree/product/use-price.tsx
Normal file
@ -0,0 +1,2 @@
|
||||
export * from '@commerce/product/use-price';
|
||||
export { default } from '@commerce/product/use-price';
|
96
lib/spree/product/use-search.tsx
Normal file
96
lib/spree/product/use-search.tsx
Normal file
@ -0,0 +1,96 @@
|
||||
import type { SWRHook } from '@commerce/utils/types';
|
||||
import useSearch from '@commerce/product/use-search';
|
||||
import type { Product, SearchProductsHook } from '@commerce/types/product';
|
||||
import type { UseSearch } from '@commerce/product/use-search';
|
||||
import normalizeProduct from '../utils/normalizations/normalize-product';
|
||||
import type { GraphQLFetcherResult } from '@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
lib/spree/provider.ts
Normal file
35
lib/spree/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;
|
166
lib/spree/types/index.ts
Normal file
166
lib/spree/types/index.ts
Normal file
@ -0,0 +1,166 @@
|
||||
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 '@commerce/types/product';
|
||||
import type {
|
||||
AddItemHook,
|
||||
RemoveItemHook,
|
||||
WishlistItemBody,
|
||||
WishlistTypes
|
||||
} from '@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 @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 };
|
||||
};
|
50
lib/spree/utils/convert-spree-error-to-graph-ql-error.ts
Normal file
50
lib/spree/utils/convert-spree-error-to-graph-ql-error.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import { FetcherError } from '@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;
|
96
lib/spree/utils/create-customized-fetch-fetcher.ts
Normal file
96
lib/spree/utils/create-customized-fetch-fetcher.ts
Normal file
@ -0,0 +1,96 @@
|
||||
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 {
|
||||
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
lib/spree/utils/create-empty-cart.ts
Normal file
22
lib/spree/utils/create-empty-cart.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { GraphQLFetcherResult } from '@commerce/api';
|
||||
import type { HookFetcherContext } from '@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;
|
22
lib/spree/utils/create-get-absolute-image-url.ts
Normal file
22
lib/spree/utils/create-get-absolute-image-url.ts
Normal file
@ -0,0 +1,22 @@
|
||||
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;
|
102
lib/spree/utils/expand-options.ts
Normal file
102
lib/spree/utils/expand-options.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import type { ProductOptionValues } from '@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;
|
42
lib/spree/utils/force-isomorphic-config-values.ts
Normal file
42
lib/spree/utils/force-isomorphic-config-values.ts
Normal file
@ -0,0 +1,42 @@
|
||||
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;
|
38
lib/spree/utils/get-image-url.ts
Normal file
38
lib/spree/utils/get-image-url.ts
Normal file
@ -0,0 +1,38 @@
|
||||
// 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;
|
21
lib/spree/utils/get-media-gallery.ts
Normal file
21
lib/spree/utils/get-media-gallery.ts
Normal file
@ -0,0 +1,21 @@
|
||||
// Based on https://github.com/spark-solutions/spree2vuestorefront/blob/d88d85ae1bcd2ec99b13b81cd2e3c25600a0216e/src/utils/index.ts
|
||||
|
||||
import type { ProductImage } from '@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
lib/spree/utils/get-product-path.ts
Normal file
7
lib/spree/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;
|
50
lib/spree/utils/get-spree-sdk-method-from-endpoint-path.ts
Normal file
50
lib/spree/utils/get-spree-sdk-method-from-endpoint-path.ts
Normal file
@ -0,0 +1,50 @@
|
||||
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
lib/spree/utils/handle-token-errors.ts
Normal file
14
lib/spree/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;
|
4
lib/spree/utils/is-json-content-type.ts
Normal file
4
lib/spree/utils/is-json-content-type.ts
Normal file
@ -0,0 +1,4 @@
|
||||
const isJsonContentType = (contentType: string): boolean =>
|
||||
contentType.includes('application/json') || contentType.includes('application/vnd.api+json');
|
||||
|
||||
export default isJsonContentType;
|
1
lib/spree/utils/is-server.ts
Normal file
1
lib/spree/utils/is-server.ts
Normal file
@ -0,0 +1 @@
|
||||
export default typeof window === 'undefined';
|
53
lib/spree/utils/login.ts
Normal file
53
lib/spree/utils/login.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import type { GraphQLFetcherResult } from '@commerce/api';
|
||||
import type { HookFetcherContext } from '@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;
|
191
lib/spree/utils/normalizations/normalize-cart.ts
Normal file
191
lib/spree/utils/normalizations/normalize-cart.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import type { Cart, LineItem, ProductVariant, SelectedOption } from '@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 '@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
lib/spree/utils/normalizations/normalize-page.ts
Normal file
42
lib/spree/utils/normalizations/normalize-page.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { Page } from '@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;
|
196
lib/spree/utils/normalizations/normalize-product.ts
Normal file
196
lib/spree/utils/normalizations/normalize-product.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import type { Product, ProductImage, ProductPrice, ProductVariant } from '@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
lib/spree/utils/normalizations/normalize-user.ts
Normal file
16
lib/spree/utils/normalizations/normalize-user.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import type { Customer } from '@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;
|
60
lib/spree/utils/normalizations/normalize-wishlist.ts
Normal file
60
lib/spree/utils/normalizations/normalize-wishlist.ts
Normal file
@ -0,0 +1,60 @@
|
||||
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;
|
14
lib/spree/utils/require-config.ts
Normal file
14
lib/spree/utils/require-config.ts
Normal file
@ -0,0 +1,14 @@
|
||||
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;
|
9
lib/spree/utils/sort-option-types.ts
Normal file
9
lib/spree/utils/sort-option-types.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import type { ExpandedProductOption } from '../types';
|
||||
|
||||
const sortOptionsByPosition = (options: ExpandedProductOption[]): ExpandedProductOption[] => {
|
||||
return options.sort((firstOption, secondOption) => {
|
||||
return firstOption.position - secondOption.position;
|
||||
});
|
||||
};
|
||||
|
||||
export default sortOptionsByPosition;
|
16
lib/spree/utils/tokens/cart-token.ts
Normal file
16
lib/spree/utils/tokens/cart-token.ts
Normal file
@ -0,0 +1,16 @@
|
||||
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);
|
||||
};
|
49
lib/spree/utils/tokens/ensure-fresh-user-access-token.ts
Normal file
49
lib/spree/utils/tokens/ensure-fresh-user-access-token.ts
Normal file
@ -0,0 +1,49 @@
|
||||
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
lib/spree/utils/tokens/ensure-itoken.ts
Normal file
25
lib/spree/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
lib/spree/utils/tokens/is-logged-in.ts
Normal file
9
lib/spree/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;
|
45
lib/spree/utils/tokens/revoke-user-tokens.ts
Normal file
45
lib/spree/utils/tokens/revoke-user-tokens.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import type { GraphQLFetcherResult } from '@commerce/api';
|
||||
import type { HookFetcherContext } from '@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;
|
50
lib/spree/utils/tokens/user-token-response.ts
Normal file
50
lib/spree/utils/tokens/user-token-response.ts
Normal file
@ -0,0 +1,50 @@
|
||||
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;
|
19
lib/spree/utils/validations/validate-cookie-expire.ts
Normal file
19
lib/spree/utils/validations/validate-cookie-expire.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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;
|
13
lib/spree/utils/validations/validate-images-option-filter.ts
Normal file
13
lib/spree/utils/validations/validate-images-option-filter.ts
Normal file
@ -0,0 +1,13 @@
|
||||
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;
|
19
lib/spree/utils/validations/validate-images-quality.ts
Normal file
19
lib/spree/utils/validations/validate-images-quality.ts
Normal file
@ -0,0 +1,19 @@
|
||||
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
lib/spree/utils/validations/validate-images-size.ts
Normal file
13
lib/spree/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,13 @@
|
||||
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,19 @@
|
||||
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
lib/spree/wishlist/index.ts
Normal file
3
lib/spree/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';
|
86
lib/spree/wishlist/use-add-item.tsx
Normal file
86
lib/spree/wishlist/use-add-item.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { MutationHook } from '@commerce/utils/types';
|
||||
import useAddItem from '@commerce/wishlist/use-add-item';
|
||||
import type { UseAddItem } from '@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 '@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 '@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.revalidate();
|
||||
|
||||
return data;
|
||||
},
|
||||
[wishlist]
|
||||
);
|
||||
};
|
||||
|
||||
return useWrappedHook;
|
||||
}
|
||||
};
|
75
lib/spree/wishlist/use-remove-item.tsx
Normal file
75
lib/spree/wishlist/use-remove-item.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { MutationHook } from '@commerce/utils/types';
|
||||
import useRemoveItem from '@commerce/wishlist/use-remove-item';
|
||||
import type { UseRemoveItem } from '@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 '@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.revalidate();
|
||||
|
||||
return data;
|
||||
},
|
||||
[wishlist]
|
||||
);
|
||||
};
|
||||
|
||||
return useWrappedHook;
|
||||
}
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user