forked from crowetic/commerce
Spree Commerce Provider (#484)
* Include @spree/storefront-api-v2-sdk * Add basic Spree framework structure * Add Spree as allowed Framework * Fetch product images, standardize API fetch using Spree SDK * Include slug and path in products * Fetch single product during build time * PLP with searching by category * Fetch Spree Categories and Brands * Sort PLP * Search products by name * Fix option values collection * Fix hasNonMasterVariants * Sort Categories and Brands * Add configuration to show product options when there's one variant available * Enable text search for the Spree Framework * Allow removing line items * Allow updating line item quantity * Add __typename to variant options to allow adding the selected variant to the cart * Use fetch and Request from node-fetch in Spree SDK * Update Spree SDK fetcher * Show placeholder message for /chechout and adjust api fetcher type * Use kebab case instead of camel case * Remove outdated comments * Remove outdated comment * Resolve isColorProductOption duplication * Type Spree variants and line items and temporarily remove height, width and depth * Remove outdated comment * Update comments about cart discounts * Remove 'spree' prefix from isomorphicConfig and add lastUpdatedProductsPrerenderCount * Implement getAllProductPaths to prerender some products during build time * Adjust fetchers to the latest Spree SDK interface * Add types to Spree taxons mapping * Revert port change in package.json scripts * Add basic README describing Spree installation * Expand README's installation section * Upgrade Spree SDK to 4.7.0 and add node-fetch to dependencies * Order providers alphanumerically Co-authored-by: Damian Legawiec <damian@sparksolutions.co> * Sort products by available_on when using the Trending sorting in useSearch * Change the default Spree port to 4000 and update README in sync with Spree Starter changes * Save primary variant's SKU when normalizing a product from Spree * Create a new cart if Spree can't find the current using a token * Add separator to README * Add missing Error subclass * Allow placeholder images for products and line items without images * Add image * Reset tsconfig.json paths to originla values * Search taxonomies by permalinks instead of IDs * Upgrade Spree SDK to version 4.7.1 * Remove references to @framework and use relative paths instead * Generalize TypeScript and add typings to getPage * Update fetcher to avoid parsing non-JSON responses * Use original product image by default instead of resized * Link to an online demo of the Spree integration in the README * Flatten fetcher responses * Include Spree in the list of supported ecommerce backends in README * Update README.md * Format Spree's README * Add link to the Spree demo site in the main README * Update README.md * Update README.md * Allow setting a taxon id for getAllProducts * Use Spree SDK's JSON:API helpers * Sort getAllProducts by -updated_at when using a taxonomy * Remove slash '/' from line item's paths * Allow filtering variant images by option type * Upgrade checkout behavior in line with core NextJS Commerce changes * Remove dummy submitCheckout function * [NX-24] Display PDP option types sorted by position from Spree * Supply Spree primary variant if a product has no option variants * Do not throw an error if a product doesn't have NEXT_PUBLIC_SPREE_IMAGES_OPTION_FILTER * [NX-43] Uses image transformations when fetching products images * Use bind to properly call Spree SDK methods and update SDK fetcher in line with SDK 4.12.0 * Fix ESLint issues in useHook * Support account sign up, login and logout Also - Converts the guest cart to a persisted cart tied to the logged in user after log in. - Fixes issues with use-remove-item. The cart will now properly refresh after an item is removed. - Uses the logged in user's token to adjust the cart and make other authenticated requests. - Transparently refreshed the access token of the logged in user with a refresh token. Replays requests to Spree which fail with a 401 error after refreshing the access token. * Fetch logged in user's cart after login or signup but associate guest cart only after signup * Support Spree default wishlist show, add and remove wished items operations * Fetch Spree CMS Pages * Fix login, handle critical token errors and fix WishlistCard Fix to WishlistCard changes its props to be consistent with WishlistButton when calling useRemoveItem * Fix variable name (#574) Variable name should be `ChevronRight` * Update get-cart.ts (#474) include digital items Co-authored-by: Gonzalo Pozzo <gonzalo.pozzo4@gmail.com> * Update normalize.ts (#475) add missing options property to `normalizeLineItem` Co-authored-by: Gonzalo Pozzo <gonzalo.pozzo4@gmail.com> * Update add-item.ts (#473) * Update add-item.ts include digital items * Update add-item.ts include digital items Co-authored-by: Gonzalo Pozzo <gonzalo.pozzo4@gmail.com> * fix typo (#572) Co-authored-by: Gonzalo Pozzo <gonzalo.pozzo4@gmail.com> * Fix authentication.refreshToken arguments * Remove redundant comments and logs * Fix createEmptyCart request to Spree and add option to disable auto login * Fix formatting issues * Apply image transformation when fetching images for products in cart * Replace call to qs with Spree SDK built-in helper * Upgrade Spree SDK to 5.0.1 * Rename zeitFetch import to vercelFetch * Abstract fetcher JSON Content-Type checking into separate function * Rename imageUrl to url getMediaGallery already provides context for the constant * Remove return type for getProductPath The return type can be trivially determined from the returned value. * Change URL to Spree demo store in root README Co-authored-by: Gonzalo Pozzo <gonzalo.pozzo4@gmail.com> * Change label for link to Spree demo store in Spree's README Co-authored-by: Gonzalo Pozzo <gonzalo.pozzo4@gmail.com> * Change URL to Spree demo store in Spree's README Co-authored-by: Gonzalo Pozzo <gonzalo.pozzo4@gmail.com> * Use only relative paths to /framework/spree from itself Co-authored-by: tniezg <tomek.niezgoda@mail.com> Co-authored-by: Damian Legawiec <damian@sparksolutions.co> Co-authored-by: Robert Nowakowski <aplegatt@gmail.com> Co-authored-by: Grey <57859708+greyhere@users.noreply.github.com> Co-authored-by: pfcodes <phil@auroradigit.al> Co-authored-by: Gonzalo Pozzo <gonzalo.pozzo4@gmail.com> Co-authored-by: Konrad Kruk <github@konradk.pl>
This commit is contained in:
parent
541009fd15
commit
d77d000431
@ -1,4 +1,4 @@
|
|||||||
# Available providers: local, bigcommerce, shopify, swell, saleor
|
# Available providers: local, bigcommerce, shopify, swell, saleor, spree
|
||||||
COMMERCE_PROVIDER=
|
COMMERCE_PROVIDER=
|
||||||
|
|
||||||
BIGCOMMERCE_STOREFRONT_API_URL=
|
BIGCOMMERCE_STOREFRONT_API_URL=
|
||||||
|
@ -13,6 +13,7 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/)
|
|||||||
- Vendure Demo: https://vendure.vercel.store
|
- Vendure Demo: https://vendure.vercel.store
|
||||||
- Saleor Demo: https://saleor.vercel.store/
|
- Saleor Demo: https://saleor.vercel.store/
|
||||||
- Ordercloud Demo: https://ordercloud.vercel.store/
|
- Ordercloud Demo: https://ordercloud.vercel.store/
|
||||||
|
- Spree Demo: https://spree.vercel.store/
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@ -28,7 +29,7 @@ Demo live at: [demo.vercel.store](https://demo.vercel.store/)
|
|||||||
|
|
||||||
## Integrations
|
## Integrations
|
||||||
|
|
||||||
Next.js Commerce integrates out-of-the-box with BigCommerce, Shopify, Swell, Saleor and Vendure. We plan to support all major ecommerce backends.
|
Next.js Commerce integrates out-of-the-box with BigCommerce, Shopify, Swell, Saleor, Vendure and Spree. We plan to support all major ecommerce backends.
|
||||||
|
|
||||||
## Considerations
|
## Considerations
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ const PROVIDERS = [
|
|||||||
'swell',
|
'swell',
|
||||||
'vendure',
|
'vendure',
|
||||||
'ordercloud',
|
'ordercloud',
|
||||||
|
'spree',
|
||||||
]
|
]
|
||||||
|
|
||||||
function getProviderName() {
|
function getProviderName() {
|
||||||
|
25
framework/spree/.env.template
Normal file
25
framework/spree/.env.template
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# Template to be used for creating .env* files (.env, .env.local etc.) in the project's root directory.
|
||||||
|
|
||||||
|
COMMERCE_PROVIDER=spree
|
||||||
|
|
||||||
|
{# - NEXT_PUBLIC_* are exposed to the web browser and the server #}
|
||||||
|
NEXT_PUBLIC_SPREE_API_HOST=http://localhost:4000
|
||||||
|
NEXT_PUBLIC_SPREE_DEFAULT_LOCALE=en-us
|
||||||
|
NEXT_PUBLIC_SPREE_CART_COOKIE_NAME=spree_cart_token
|
||||||
|
{# -- cookie expire in days #}
|
||||||
|
NEXT_PUBLIC_SPREE_CART_COOKIE_EXPIRE=7
|
||||||
|
NEXT_PUBLIC_SPREE_USER_COOKIE_NAME=spree_user_token
|
||||||
|
NEXT_PUBLIC_SPREE_USER_COOKIE_EXPIRE=7
|
||||||
|
NEXT_PUBLIC_SPREE_IMAGE_HOST=http://localhost:4000
|
||||||
|
NEXT_PUBLIC_SPREE_ALLOWED_IMAGE_DOMAIN=localhost
|
||||||
|
NEXT_PUBLIC_SPREE_CATEGORIES_TAXONOMY_PERMALINK=categories
|
||||||
|
NEXT_PUBLIC_SPREE_BRANDS_TAXONOMY_PERMALINK=brands
|
||||||
|
NEXT_PUBLIC_SPREE_ALL_PRODUCTS_TAXONOMY_ID=false
|
||||||
|
NEXT_PUBLIC_SPREE_SHOW_SINGLE_VARIANT_OPTIONS=false
|
||||||
|
NEXT_PUBLIC_SPREE_LAST_UPDATED_PRODUCTS_PRERENDER_COUNT=10
|
||||||
|
NEXT_PUBLIC_SPREE_PRODUCT_PLACEHOLDER_IMAGE_URL=/product-img-placeholder.svg
|
||||||
|
NEXT_PUBLIC_SPREE_LINE_ITEM_PLACEHOLDER_IMAGE_URL=/product-img-placeholder.svg
|
||||||
|
NEXT_PUBLIC_SPREE_IMAGES_OPTION_FILTER=false
|
||||||
|
NEXT_PUBLIC_SPREE_IMAGES_SIZE=1000x1000
|
||||||
|
NEXT_PUBLIC_SPREE_IMAGES_QUALITY=100
|
||||||
|
NEXT_PUBLIC_SPREE_LOGIN_AFTER_SIGNUP=true
|
BIN
framework/spree/README-assets/screenshots.png
Normal file
BIN
framework/spree/README-assets/screenshots.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 114 KiB |
33
framework/spree/README.md
Normal file
33
framework/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 `yarn 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
framework/spree/api/endpoints/cart/index.ts
Normal file
1
framework/spree/api/endpoints/cart/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
framework/spree/api/endpoints/catalog/index.ts
Normal file
1
framework/spree/api/endpoints/catalog/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
framework/spree/api/endpoints/catalog/products.ts
Normal file
1
framework/spree/api/endpoints/catalog/products.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
44
framework/spree/api/endpoints/checkout/get-checkout.ts
Normal file
44
framework/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
|
22
framework/spree/api/endpoints/checkout/index.ts
Normal file
22
framework/spree/api/endpoints/checkout/index.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
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
framework/spree/api/endpoints/customer/address.ts
Normal file
1
framework/spree/api/endpoints/customer/address.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
framework/spree/api/endpoints/customer/card.ts
Normal file
1
framework/spree/api/endpoints/customer/card.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
framework/spree/api/endpoints/customer/index.ts
Normal file
1
framework/spree/api/endpoints/customer/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
framework/spree/api/endpoints/login/index.ts
Normal file
1
framework/spree/api/endpoints/login/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
framework/spree/api/endpoints/logout/index.ts
Normal file
1
framework/spree/api/endpoints/logout/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
framework/spree/api/endpoints/signup/index.ts
Normal file
1
framework/spree/api/endpoints/signup/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
1
framework/spree/api/endpoints/wishlist/index.tsx
Normal file
1
framework/spree/api/endpoints/wishlist/index.tsx
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default function noopApi(...args: any[]): void {}
|
45
framework/spree/api/index.ts
Normal file
45
framework/spree/api/index.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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'
|
||||||
|
|
||||||
|
export interface SpreeApiConfig extends CommerceAPIConfig {}
|
||||||
|
|
||||||
|
const config: SpreeApiConfig = {
|
||||||
|
commerceUrl: '',
|
||||||
|
apiToken: '',
|
||||||
|
cartCookie: '',
|
||||||
|
customerCookie: '',
|
||||||
|
cartCookieMaxAge: 2592000,
|
||||||
|
fetch: createApiFetch(() => getCommerceApi().getConfig()),
|
||||||
|
}
|
||||||
|
|
||||||
|
const operations = {
|
||||||
|
getAllPages,
|
||||||
|
getPage,
|
||||||
|
getSiteInfo,
|
||||||
|
getCustomerWishlist,
|
||||||
|
getAllProductPaths,
|
||||||
|
getAllProducts,
|
||||||
|
getProduct,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const provider = { config, operations }
|
||||||
|
|
||||||
|
export type SpreeApiProvider = typeof provider
|
||||||
|
|
||||||
|
export type SpreeApi<P extends SpreeApiProvider = SpreeApiProvider> =
|
||||||
|
CommerceAPI<P>
|
||||||
|
|
||||||
|
export function getCommerceApi<P extends SpreeApiProvider>(
|
||||||
|
customProvider: P = provider as any
|
||||||
|
): SpreeApi<P> {
|
||||||
|
return commerceApi(customProvider)
|
||||||
|
}
|
82
framework/spree/api/operations/get-all-pages.ts
Normal file
82
framework/spree/api/operations/get-all-pages.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
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
|
||||||
|
}
|
97
framework/spree/api/operations/get-all-product-paths.ts
Normal file
97
framework/spree/api/operations/get-all-product-paths.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
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
|
||||||
|
}
|
92
framework/spree/api/operations/get-all-products.ts
Normal file
92
framework/spree/api/operations/get-all-products.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
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
|
||||||
|
}
|
6
framework/spree/api/operations/get-customer-wishlist.ts
Normal file
6
framework/spree/api/operations/get-customer-wishlist.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default function getCustomerWishlistOperation() {
|
||||||
|
function getCustomerWishlist(): any {
|
||||||
|
return { wishlist: {} }
|
||||||
|
}
|
||||||
|
return getCustomerWishlist
|
||||||
|
}
|
81
framework/spree/api/operations/get-page.ts
Normal file
81
framework/spree/api/operations/get-page.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
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
|
||||||
|
}
|
90
framework/spree/api/operations/get-product.ts
Normal file
90
framework/spree/api/operations/get-product.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
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
|
||||||
|
}
|
135
framework/spree/api/operations/get-site-info.ts
Normal file
135
framework/spree/api/operations/get-site-info.ts
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
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
|
||||||
|
}
|
6
framework/spree/api/operations/index.ts
Normal file
6
framework/spree/api/operations/index.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export { default as getPage } from './get-page'
|
||||||
|
export { default as getSiteInfo } from './get-site-info'
|
||||||
|
export { default as getAllPages } from './get-all-pages'
|
||||||
|
export { default as getProduct } from './get-product'
|
||||||
|
export { default as getAllProducts } from './get-all-products'
|
||||||
|
export { default as getAllProductPaths } from './get-all-product-paths'
|
79
framework/spree/api/utils/create-api-fetch.ts
Normal file
79
framework/spree/api/utils/create-api-fetch.ts
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
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
framework/spree/api/utils/fetch.ts
Normal file
3
framework/spree/api/utils/fetch.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import vercelFetch from '@vercel/fetch'
|
||||||
|
|
||||||
|
export default vercelFetch()
|
3
framework/spree/auth/index.ts
Normal file
3
framework/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'
|
85
framework/spree/auth/use-login.tsx
Normal file
85
framework/spree/auth/use-login.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
80
framework/spree/auth/use-logout.tsx
Normal file
80
framework/spree/auth/use-logout.tsx
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
95
framework/spree/auth/use-signup.tsx
Normal file
95
framework/spree/auth/use-signup.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
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
framework/spree/cart/index.ts
Normal file
4
framework/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'
|
117
framework/spree/cart/use-add-item.tsx
Normal file
117
framework/spree/cart/use-add-item.tsx
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
123
framework/spree/cart/use-cart.tsx
Normal file
123
framework/spree/cart/use-cart.tsx
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
118
framework/spree/cart/use-remove-item.tsx
Normal file
118
framework/spree/cart/use-remove-item.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
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
|
||||||
|
},
|
||||||
|
}
|
145
framework/spree/cart/use-update-item.tsx
Normal file
145
framework/spree/cart/use-update-item.tsx
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
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
framework/spree/checkout/use-checkout.tsx
Normal file
17
framework/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
framework/spree/commerce.config.json
Normal file
10
framework/spree/commerce.config.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"provider": "spree",
|
||||||
|
"features": {
|
||||||
|
"wishlist": true,
|
||||||
|
"cart": true,
|
||||||
|
"search": true,
|
||||||
|
"customerAuth": true,
|
||||||
|
"customCheckout": false
|
||||||
|
}
|
||||||
|
}
|
18
framework/spree/customer/address/use-add-item.tsx
Normal file
18
framework/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
framework/spree/customer/card/use-add-item.tsx
Normal file
19
framework/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
framework/spree/customer/index.ts
Normal file
1
framework/spree/customer/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as useCustomer } from './use-customer'
|
83
framework/spree/customer/use-customer.tsx
Normal file
83
framework/spree/customer/use-customer.tsx
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
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
framework/spree/errors/AccessTokenError.ts
Normal file
1
framework/spree/errors/AccessTokenError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class AccessTokenError extends Error {}
|
1
framework/spree/errors/MisconfigurationError.ts
Normal file
1
framework/spree/errors/MisconfigurationError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class MisconfigurationError extends Error {}
|
1
framework/spree/errors/MissingConfigurationValueError.ts
Normal file
1
framework/spree/errors/MissingConfigurationValueError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class MissingConfigurationValueError extends Error {}
|
1
framework/spree/errors/MissingLineItemVariantError.ts
Normal file
1
framework/spree/errors/MissingLineItemVariantError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class MissingLineItemVariantError extends Error {}
|
1
framework/spree/errors/MissingOptionValueError.ts
Normal file
1
framework/spree/errors/MissingOptionValueError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class MissingOptionValueError extends Error {}
|
1
framework/spree/errors/MissingPrimaryVariantError.ts
Normal file
1
framework/spree/errors/MissingPrimaryVariantError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class MissingPrimaryVariantError extends Error {}
|
1
framework/spree/errors/MissingProductError.ts
Normal file
1
framework/spree/errors/MissingProductError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class MissingProductError extends Error {}
|
1
framework/spree/errors/MissingSlugVariableError.ts
Normal file
1
framework/spree/errors/MissingSlugVariableError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class MissingSlugVariableError extends Error {}
|
1
framework/spree/errors/MissingVariantError.ts
Normal file
1
framework/spree/errors/MissingVariantError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class MissingVariantError extends Error {}
|
1
framework/spree/errors/RefreshTokenError.ts
Normal file
1
framework/spree/errors/RefreshTokenError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class RefreshTokenError extends Error {}
|
1
framework/spree/errors/SpreeResponseContentError.ts
Normal file
1
framework/spree/errors/SpreeResponseContentError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class SpreeResponseContentError extends Error {}
|
@ -0,0 +1 @@
|
|||||||
|
export default class SpreeSdkMethodFromEndpointPathError extends Error {}
|
1
framework/spree/errors/TokensNotRejectedError.ts
Normal file
1
framework/spree/errors/TokensNotRejectedError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class TokensNotRejectedError extends Error {}
|
1
framework/spree/errors/UserTokenResponseParseError.ts
Normal file
1
framework/spree/errors/UserTokenResponseParseError.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default class UserTokenResponseParseError extends Error {}
|
116
framework/spree/fetcher.ts
Normal file
116
framework/spree/fetcher.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import type { Fetcher } from '@commerce/utils/types'
|
||||||
|
import convertSpreeErrorToGraphQlError from './utils/convert-spree-error-to-graph-ql-error'
|
||||||
|
import { makeClient, errors } from '@spree/storefront-api-v2-sdk'
|
||||||
|
import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse'
|
||||||
|
import type { GraphQLFetcherResult } from '@commerce/api'
|
||||||
|
import { requireConfigValue } from './isomorphic-config'
|
||||||
|
import getSpreeSdkMethodFromEndpointPath from './utils/get-spree-sdk-method-from-endpoint-path'
|
||||||
|
import SpreeSdkMethodFromEndpointPathError from './errors/SpreeSdkMethodFromEndpointPathError'
|
||||||
|
import type {
|
||||||
|
FetcherVariables,
|
||||||
|
SpreeSdkResponse,
|
||||||
|
SpreeSdkResponseWithRawResponse,
|
||||||
|
} from './types'
|
||||||
|
import createCustomizedFetchFetcher, {
|
||||||
|
fetchResponseKey,
|
||||||
|
} from './utils/create-customized-fetch-fetcher'
|
||||||
|
import ensureFreshUserAccessToken from './utils/tokens/ensure-fresh-user-access-token'
|
||||||
|
import RefreshTokenError from './errors/RefreshTokenError'
|
||||||
|
|
||||||
|
const client = makeClient({
|
||||||
|
host: requireConfigValue('apiHost') as string,
|
||||||
|
createFetcher: (fetcherOptions) => {
|
||||||
|
return createCustomizedFetchFetcher({
|
||||||
|
fetch: globalThis.fetch,
|
||||||
|
requestConstructor: globalThis.Request,
|
||||||
|
...fetcherOptions,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const normalizeSpreeSuccessResponse = (
|
||||||
|
storeResponse: ResultResponse<SpreeSdkResponseWithRawResponse>
|
||||||
|
): GraphQLFetcherResult<SpreeSdkResponse> => {
|
||||||
|
const data = storeResponse.success()
|
||||||
|
const rawFetchResponse = data[fetchResponseKey]
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
res: rawFetchResponse,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetcher: Fetcher<GraphQLFetcherResult<SpreeSdkResponse>> = async (
|
||||||
|
requestOptions
|
||||||
|
) => {
|
||||||
|
const { url, method, variables, query } = requestOptions
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'Fetcher called. Configuration: ',
|
||||||
|
'url = ',
|
||||||
|
url,
|
||||||
|
'requestOptions = ',
|
||||||
|
requestOptions
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!variables) {
|
||||||
|
throw new SpreeSdkMethodFromEndpointPathError(
|
||||||
|
`Required FetcherVariables not provided.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
methodPath,
|
||||||
|
arguments: args,
|
||||||
|
refreshExpiredAccessToken = true,
|
||||||
|
replayUnauthorizedRequest = true,
|
||||||
|
} = variables as FetcherVariables
|
||||||
|
|
||||||
|
if (refreshExpiredAccessToken) {
|
||||||
|
await ensureFreshUserAccessToken(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
const spreeSdkMethod = getSpreeSdkMethodFromEndpointPath(client, methodPath)
|
||||||
|
|
||||||
|
const storeResponse: ResultResponse<SpreeSdkResponseWithRawResponse> =
|
||||||
|
await spreeSdkMethod(...args)
|
||||||
|
|
||||||
|
if (storeResponse.isSuccess()) {
|
||||||
|
return normalizeSpreeSuccessResponse(storeResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
const storeResponseError = storeResponse.fail()
|
||||||
|
|
||||||
|
if (
|
||||||
|
storeResponseError instanceof errors.SpreeError &&
|
||||||
|
storeResponseError.serverResponse.status === 401 &&
|
||||||
|
replayUnauthorizedRequest
|
||||||
|
) {
|
||||||
|
console.info(
|
||||||
|
'Request ended with 401. Replaying request after refreshing the user token.'
|
||||||
|
)
|
||||||
|
|
||||||
|
await ensureFreshUserAccessToken(client)
|
||||||
|
|
||||||
|
const replayedStoreResponse: ResultResponse<SpreeSdkResponseWithRawResponse> =
|
||||||
|
await spreeSdkMethod(...args)
|
||||||
|
|
||||||
|
if (replayedStoreResponse.isSuccess()) {
|
||||||
|
return normalizeSpreeSuccessResponse(replayedStoreResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn('Replaying the request failed', replayedStoreResponse.fail())
|
||||||
|
|
||||||
|
throw new RefreshTokenError(
|
||||||
|
'Could not authorize request with current access token.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (storeResponseError instanceof errors.SpreeError) {
|
||||||
|
throw convertSpreeErrorToGraphQlError(storeResponseError)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw storeResponseError
|
||||||
|
}
|
||||||
|
|
||||||
|
export default fetcher
|
49
framework/spree/index.tsx
Normal file
49
framework/spree/index.tsx
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import type { ComponentType, FunctionComponent } from 'react'
|
||||||
|
import {
|
||||||
|
Provider,
|
||||||
|
CommerceProviderProps,
|
||||||
|
CoreCommerceProvider,
|
||||||
|
useCommerce as useCoreCommerce,
|
||||||
|
} from '@commerce'
|
||||||
|
import { spreeProvider } from './provider'
|
||||||
|
import type { SpreeProvider } from './provider'
|
||||||
|
import { SWRConfig } from 'swr'
|
||||||
|
import handleTokenErrors from './utils/handle-token-errors'
|
||||||
|
import useLogout from '@commerce/auth/use-logout'
|
||||||
|
|
||||||
|
export { spreeProvider }
|
||||||
|
export type { SpreeProvider }
|
||||||
|
|
||||||
|
export const WithTokenErrorsHandling: FunctionComponent = ({ children }) => {
|
||||||
|
const logout = useLogout()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SWRConfig
|
||||||
|
value={{
|
||||||
|
onError: (error, _key) => {
|
||||||
|
handleTokenErrors(error, () => void logout())
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SWRConfig>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCommerceProvider = <P extends Provider>(provider: P) => {
|
||||||
|
return function CommerceProvider({
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: CommerceProviderProps) {
|
||||||
|
return (
|
||||||
|
<CoreCommerceProvider provider={{ ...provider, ...props }}>
|
||||||
|
<WithTokenErrorsHandling>{children}</WithTokenErrorsHandling>
|
||||||
|
</CoreCommerceProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CommerceProvider =
|
||||||
|
getCommerceProvider<SpreeProvider>(spreeProvider)
|
||||||
|
|
||||||
|
export const useCommerce = () => useCoreCommerce<SpreeProvider>()
|
81
framework/spree/isomorphic-config.ts
Normal file
81
framework/spree/isomorphic-config.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import forceIsomorphicConfigValues from './utils/force-isomorphic-config-values'
|
||||||
|
import requireConfig from './utils/require-config'
|
||||||
|
import validateAllProductsTaxonomyId from './utils/validations/validate-all-products-taxonomy-id'
|
||||||
|
import validateCookieExpire from './utils/validations/validate-cookie-expire'
|
||||||
|
import validateImagesOptionFilter from './utils/validations/validate-images-option-filter'
|
||||||
|
import validatePlaceholderImageUrl from './utils/validations/validate-placeholder-image-url'
|
||||||
|
import validateProductsPrerenderCount from './utils/validations/validate-products-prerender-count'
|
||||||
|
import validateImagesSize from './utils/validations/validate-images-size'
|
||||||
|
import validateImagesQuality from './utils/validations/validate-images-quality'
|
||||||
|
|
||||||
|
const isomorphicConfig = {
|
||||||
|
apiHost: process.env.NEXT_PUBLIC_SPREE_API_HOST,
|
||||||
|
defaultLocale: process.env.NEXT_PUBLIC_SPREE_DEFAULT_LOCALE,
|
||||||
|
cartCookieName: process.env.NEXT_PUBLIC_SPREE_CART_COOKIE_NAME,
|
||||||
|
cartCookieExpire: validateCookieExpire(
|
||||||
|
process.env.NEXT_PUBLIC_SPREE_CART_COOKIE_EXPIRE
|
||||||
|
),
|
||||||
|
userCookieName: process.env.NEXT_PUBLIC_SPREE_USER_COOKIE_NAME,
|
||||||
|
userCookieExpire: validateCookieExpire(
|
||||||
|
process.env.NEXT_PUBLIC_SPREE_CART_COOKIE_EXPIRE
|
||||||
|
),
|
||||||
|
imageHost: process.env.NEXT_PUBLIC_SPREE_IMAGE_HOST,
|
||||||
|
categoriesTaxonomyPermalink:
|
||||||
|
process.env.NEXT_PUBLIC_SPREE_CATEGORIES_TAXONOMY_PERMALINK,
|
||||||
|
brandsTaxonomyPermalink:
|
||||||
|
process.env.NEXT_PUBLIC_SPREE_BRANDS_TAXONOMY_PERMALINK,
|
||||||
|
allProductsTaxonomyId: validateAllProductsTaxonomyId(
|
||||||
|
process.env.NEXT_PUBLIC_SPREE_ALL_PRODUCTS_TAXONOMY_ID
|
||||||
|
),
|
||||||
|
showSingleVariantOptions:
|
||||||
|
process.env.NEXT_PUBLIC_SPREE_SHOW_SINGLE_VARIANT_OPTIONS === 'true',
|
||||||
|
lastUpdatedProductsPrerenderCount: validateProductsPrerenderCount(
|
||||||
|
process.env.NEXT_PUBLIC_SPREE_LAST_UPDATED_PRODUCTS_PRERENDER_COUNT
|
||||||
|
),
|
||||||
|
productPlaceholderImageUrl: validatePlaceholderImageUrl(
|
||||||
|
process.env.NEXT_PUBLIC_SPREE_PRODUCT_PLACEHOLDER_IMAGE_URL
|
||||||
|
),
|
||||||
|
lineItemPlaceholderImageUrl: validatePlaceholderImageUrl(
|
||||||
|
process.env.NEXT_PUBLIC_SPREE_LINE_ITEM_PLACEHOLDER_IMAGE_URL
|
||||||
|
),
|
||||||
|
imagesOptionFilter: validateImagesOptionFilter(
|
||||||
|
process.env.NEXT_PUBLIC_SPREE_IMAGES_OPTION_FILTER
|
||||||
|
),
|
||||||
|
imagesSize: validateImagesSize(process.env.NEXT_PUBLIC_SPREE_IMAGES_SIZE),
|
||||||
|
imagesQuality: validateImagesQuality(
|
||||||
|
process.env.NEXT_PUBLIC_SPREE_IMAGES_QUALITY
|
||||||
|
),
|
||||||
|
loginAfterSignup: process.env.NEXT_PUBLIC_SPREE_LOGIN_AFTER_SIGNUP === 'true',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forceIsomorphicConfigValues(
|
||||||
|
isomorphicConfig,
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
'apiHost',
|
||||||
|
'defaultLocale',
|
||||||
|
'cartCookieName',
|
||||||
|
'cartCookieExpire',
|
||||||
|
'userCookieName',
|
||||||
|
'userCookieExpire',
|
||||||
|
'imageHost',
|
||||||
|
'categoriesTaxonomyPermalink',
|
||||||
|
'brandsTaxonomyPermalink',
|
||||||
|
'allProductsTaxonomyId',
|
||||||
|
'showSingleVariantOptions',
|
||||||
|
'lastUpdatedProductsPrerenderCount',
|
||||||
|
'productPlaceholderImageUrl',
|
||||||
|
'lineItemPlaceholderImageUrl',
|
||||||
|
'imagesOptionFilter',
|
||||||
|
'imagesSize',
|
||||||
|
'imagesQuality',
|
||||||
|
'loginAfterSignup',
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
type IsomorphicConfig = typeof isomorphicConfig
|
||||||
|
|
||||||
|
const requireConfigValue = (key: keyof IsomorphicConfig) =>
|
||||||
|
requireConfig<IsomorphicConfig>(isomorphicConfig, key)
|
||||||
|
|
||||||
|
export { requireConfigValue }
|
16
framework/spree/next.config.js
Normal file
16
framework/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
framework/spree/product/index.ts
Normal file
2
framework/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
framework/spree/product/use-price.tsx
Normal file
2
framework/spree/product/use-price.tsx
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from '@commerce/product/use-price'
|
||||||
|
export { default } from '@commerce/product/use-price'
|
101
framework/spree/product/use-search.tsx
Normal file
101
framework/spree/product/use-search.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
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
framework/spree/provider.ts
Normal file
35
framework/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
|
164
framework/spree/types/index.ts
Normal file
164
framework/spree/types/index.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import type { fetchResponseKey } from '../utils/create-customized-fetch-fetcher'
|
||||||
|
import type {
|
||||||
|
JsonApiDocument,
|
||||||
|
JsonApiListResponse,
|
||||||
|
JsonApiSingleResponse,
|
||||||
|
} from '@spree/storefront-api-v2-sdk/types/interfaces/JsonApi'
|
||||||
|
import type { ResultResponse } from '@spree/storefront-api-v2-sdk/types/interfaces/ResultResponse'
|
||||||
|
import type { Response } from '@vercel/fetch'
|
||||||
|
import type { ProductOption, Product } from '@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 }
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
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
|
105
framework/spree/utils/create-customized-fetch-fetcher.ts
Normal file
105
framework/spree/utils/create-customized-fetch-fetcher.ts
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
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
framework/spree/utils/create-empty-cart.ts
Normal file
22
framework/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
|
26
framework/spree/utils/create-get-absolute-image-url.ts
Normal file
26
framework/spree/utils/create-get-absolute-image-url.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { SpreeProductImage } from '../types'
|
||||||
|
import getImageUrl from './get-image-url'
|
||||||
|
|
||||||
|
const createGetAbsoluteImageUrl =
|
||||||
|
(host: string, useOriginalImageSize: boolean = true) =>
|
||||||
|
(
|
||||||
|
image: SpreeProductImage,
|
||||||
|
minWidth: number,
|
||||||
|
minHeight: number
|
||||||
|
): string | null => {
|
||||||
|
let url
|
||||||
|
|
||||||
|
if (useOriginalImageSize) {
|
||||||
|
url = image.attributes.transformed_url || null
|
||||||
|
} else {
|
||||||
|
url = getImageUrl(image, minWidth, minHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${host}${url}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createGetAbsoluteImageUrl
|
103
framework/spree/utils/expand-options.ts
Normal file
103
framework/spree/utils/expand-options.ts
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
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
|
43
framework/spree/utils/force-isomorphic-config-values.ts
Normal file
43
framework/spree/utils/force-isomorphic-config-values.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import type { NonUndefined, UnknownObjectValues } from '../types'
|
||||||
|
import MisconfigurationError from '../errors/MisconfigurationError'
|
||||||
|
import isServer from './is-server'
|
||||||
|
|
||||||
|
const generateMisconfigurationErrorMessage = (
|
||||||
|
keys: Array<string | number | symbol>
|
||||||
|
) => `${keys.join(', ')} must have a value before running the Framework.`
|
||||||
|
|
||||||
|
const forceIsomorphicConfigValues = <
|
||||||
|
X extends keyof T,
|
||||||
|
T extends UnknownObjectValues,
|
||||||
|
H extends Record<X, NonUndefined<T[X]>>
|
||||||
|
>(
|
||||||
|
config: T,
|
||||||
|
requiredServerKeys: string[],
|
||||||
|
requiredPublicKeys: X[]
|
||||||
|
) => {
|
||||||
|
if (isServer) {
|
||||||
|
const missingServerConfigValues = requiredServerKeys.filter(
|
||||||
|
(requiredServerKey) => typeof config[requiredServerKey] === 'undefined'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (missingServerConfigValues.length > 0) {
|
||||||
|
throw new MisconfigurationError(
|
||||||
|
generateMisconfigurationErrorMessage(missingServerConfigValues)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingPublicConfigValues = requiredPublicKeys.filter(
|
||||||
|
(requiredPublicKey) => typeof config[requiredPublicKey] === 'undefined'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (missingPublicConfigValues.length > 0) {
|
||||||
|
throw new MisconfigurationError(
|
||||||
|
generateMisconfigurationErrorMessage(missingPublicConfigValues)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return config as T & H
|
||||||
|
}
|
||||||
|
|
||||||
|
export default forceIsomorphicConfigValues
|
44
framework/spree/utils/get-image-url.ts
Normal file
44
framework/spree/utils/get-image-url.ts
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Based on https://github.com/spark-solutions/spree2vuestorefront/blob/d88d85ae1bcd2ec99b13b81cd2e3c25600a0216e/src/utils/index.ts
|
||||||
|
|
||||||
|
import type { ImageStyle, SpreeProductImage } from '../types'
|
||||||
|
|
||||||
|
const getImageUrl = (
|
||||||
|
image: SpreeProductImage,
|
||||||
|
minWidth: number,
|
||||||
|
_: number
|
||||||
|
): string | null => {
|
||||||
|
// every image is still resized in vue-storefront-api, no matter what getImageUrl returns
|
||||||
|
if (image) {
|
||||||
|
const {
|
||||||
|
attributes: { styles },
|
||||||
|
} = image
|
||||||
|
const bestStyleIndex = styles.reduce(
|
||||||
|
(bSIndex: number | null, style: ImageStyle, styleIndex: number) => {
|
||||||
|
// assuming all images are the same dimensions, just scaled
|
||||||
|
if (bSIndex === null) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const bestStyle = styles[bSIndex]
|
||||||
|
const widthDiff = +bestStyle.width - minWidth
|
||||||
|
const minWidthDiff = +style.width - minWidth
|
||||||
|
if (widthDiff < 0 && minWidthDiff > 0) {
|
||||||
|
return styleIndex
|
||||||
|
}
|
||||||
|
if (widthDiff > 0 && minWidthDiff < 0) {
|
||||||
|
return bSIndex
|
||||||
|
}
|
||||||
|
return Math.abs(widthDiff) < Math.abs(minWidthDiff)
|
||||||
|
? bSIndex
|
||||||
|
: styleIndex
|
||||||
|
},
|
||||||
|
null
|
||||||
|
)
|
||||||
|
|
||||||
|
if (bestStyleIndex !== null) {
|
||||||
|
return styles[bestStyleIndex].url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getImageUrl
|
25
framework/spree/utils/get-media-gallery.ts
Normal file
25
framework/spree/utils/get-media-gallery.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// Based on https://github.com/spark-solutions/spree2vuestorefront/blob/d88d85ae1bcd2ec99b13b81cd2e3c25600a0216e/src/utils/index.ts
|
||||||
|
|
||||||
|
import type { ProductImage } from '@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
framework/spree/utils/get-product-path.ts
Normal file
7
framework/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
|
@ -0,0 +1,61 @@
|
|||||||
|
import type { Client } from '@spree/storefront-api-v2-sdk'
|
||||||
|
import SpreeSdkMethodFromEndpointPathError from '../errors/SpreeSdkMethodFromEndpointPathError'
|
||||||
|
import type {
|
||||||
|
SpreeSdkMethod,
|
||||||
|
SpreeSdkResultResponseSuccessType,
|
||||||
|
} from '../types'
|
||||||
|
|
||||||
|
const getSpreeSdkMethodFromEndpointPath = <
|
||||||
|
ExactSpreeSdkClientType extends Client,
|
||||||
|
ResultResponseSuccessType extends SpreeSdkResultResponseSuccessType = SpreeSdkResultResponseSuccessType
|
||||||
|
>(
|
||||||
|
client: ExactSpreeSdkClientType,
|
||||||
|
path: string
|
||||||
|
): SpreeSdkMethod<ResultResponseSuccessType> => {
|
||||||
|
const pathParts = path.split('.')
|
||||||
|
const reachedPath: string[] = []
|
||||||
|
let node = <Record<string, unknown>>client
|
||||||
|
|
||||||
|
console.log(`Looking for ${path} in Spree Sdk.`)
|
||||||
|
|
||||||
|
while (reachedPath.length < pathParts.length - 1) {
|
||||||
|
const checkedPathPart = pathParts[reachedPath.length]
|
||||||
|
const checkedNode = node[checkedPathPart]
|
||||||
|
|
||||||
|
console.log(`Checking part ${checkedPathPart}.`)
|
||||||
|
|
||||||
|
if (typeof checkedNode !== 'object') {
|
||||||
|
throw new SpreeSdkMethodFromEndpointPathError(
|
||||||
|
`Couldn't reach ${path}. Farthest path reached was: ${reachedPath.join(
|
||||||
|
'.'
|
||||||
|
)}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checkedNode === null) {
|
||||||
|
throw new SpreeSdkMethodFromEndpointPathError(
|
||||||
|
`Path ${path} doesn't exist.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
node = <Record<string, unknown>>checkedNode
|
||||||
|
reachedPath.push(checkedPathPart)
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundEndpointMethod = node[pathParts[reachedPath.length]]
|
||||||
|
|
||||||
|
if (
|
||||||
|
reachedPath.length !== pathParts.length - 1 ||
|
||||||
|
typeof foundEndpointMethod !== 'function'
|
||||||
|
) {
|
||||||
|
throw new SpreeSdkMethodFromEndpointPathError(
|
||||||
|
`Couldn't reach ${path}. Farthest path reached was: ${reachedPath.join(
|
||||||
|
'.'
|
||||||
|
)}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return foundEndpointMethod.bind(node)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getSpreeSdkMethodFromEndpointPath
|
14
framework/spree/utils/handle-token-errors.ts
Normal file
14
framework/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
|
5
framework/spree/utils/is-json-content-type.ts
Normal file
5
framework/spree/utils/is-json-content-type.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const isJsonContentType = (contentType: string): boolean =>
|
||||||
|
contentType.includes('application/json') ||
|
||||||
|
contentType.includes('application/vnd.api+json')
|
||||||
|
|
||||||
|
export default isJsonContentType
|
1
framework/spree/utils/is-server.ts
Normal file
1
framework/spree/utils/is-server.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export default typeof window === 'undefined'
|
58
framework/spree/utils/login.ts
Normal file
58
framework/spree/utils/login.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
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
|
211
framework/spree/utils/normalizations/normalize-cart.ts
Normal file
211
framework/spree/utils/normalizations/normalize-cart.ts
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
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
framework/spree/utils/normalizations/normalize-page.ts
Normal file
42
framework/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
|
240
framework/spree/utils/normalizations/normalize-product.ts
Normal file
240
framework/spree/utils/normalizations/normalize-product.ts
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
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
framework/spree/utils/normalizations/normalize-user.ts
Normal file
16
framework/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
|
68
framework/spree/utils/normalizations/normalize-wishlist.ts
Normal file
68
framework/spree/utils/normalizations/normalize-wishlist.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import MissingProductError from '../../errors/MissingProductError'
|
||||||
|
import MissingVariantError from '../../errors/MissingVariantError'
|
||||||
|
import { jsonApi } from '@spree/storefront-api-v2-sdk'
|
||||||
|
import type { ProductAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Product'
|
||||||
|
import type { WishedItemAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/WishedItem'
|
||||||
|
import type { WishlistAttr } from '@spree/storefront-api-v2-sdk/types/interfaces/Wishlist'
|
||||||
|
import type {
|
||||||
|
ExplicitCommerceWishlist,
|
||||||
|
SpreeSdkResponse,
|
||||||
|
VariantAttr,
|
||||||
|
} from '../../types'
|
||||||
|
import normalizeProduct from './normalize-product'
|
||||||
|
|
||||||
|
const normalizeWishlist = (
|
||||||
|
spreeSuccessResponse: SpreeSdkResponse,
|
||||||
|
spreeWishlist: WishlistAttr
|
||||||
|
): ExplicitCommerceWishlist => {
|
||||||
|
const spreeWishedItems = jsonApi.findRelationshipDocuments<WishedItemAttr>(
|
||||||
|
spreeSuccessResponse,
|
||||||
|
spreeWishlist,
|
||||||
|
'wished_items'
|
||||||
|
)
|
||||||
|
|
||||||
|
const items: ExplicitCommerceWishlist['items'] = spreeWishedItems.map(
|
||||||
|
(spreeWishedItem) => {
|
||||||
|
const spreeWishedVariant =
|
||||||
|
jsonApi.findSingleRelationshipDocument<VariantAttr>(
|
||||||
|
spreeSuccessResponse,
|
||||||
|
spreeWishedItem,
|
||||||
|
'variant'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (spreeWishedVariant === null) {
|
||||||
|
throw new MissingVariantError(
|
||||||
|
`Couldn't find variant for wished item with id ${spreeWishedItem.id}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const spreeWishedProduct =
|
||||||
|
jsonApi.findSingleRelationshipDocument<ProductAttr>(
|
||||||
|
spreeSuccessResponse,
|
||||||
|
spreeWishedVariant,
|
||||||
|
'product'
|
||||||
|
)
|
||||||
|
|
||||||
|
if (spreeWishedProduct === null) {
|
||||||
|
throw new MissingProductError(
|
||||||
|
`Couldn't find product for variant with id ${spreeWishedVariant.id}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: spreeWishedItem.id,
|
||||||
|
product_id: parseInt(spreeWishedProduct.id, 10),
|
||||||
|
variant_id: parseInt(spreeWishedVariant.id, 10),
|
||||||
|
product: normalizeProduct(spreeSuccessResponse, spreeWishedProduct),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: spreeWishlist.id,
|
||||||
|
token: spreeWishlist.attributes.token,
|
||||||
|
items,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default normalizeWishlist
|
16
framework/spree/utils/require-config.ts
Normal file
16
framework/spree/utils/require-config.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import MissingConfigurationValueError from '../errors/MissingConfigurationValueError'
|
||||||
|
import type { NonUndefined, ValueOf } from '../types'
|
||||||
|
|
||||||
|
const requireConfig = <T>(isomorphicConfig: T, key: keyof T) => {
|
||||||
|
const valueUnderKey = isomorphicConfig[key]
|
||||||
|
|
||||||
|
if (typeof valueUnderKey === 'undefined') {
|
||||||
|
throw new MissingConfigurationValueError(
|
||||||
|
`Value for configuration key ${key} was undefined.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueUnderKey as NonUndefined<ValueOf<T>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default requireConfig
|
11
framework/spree/utils/sort-option-types.ts
Normal file
11
framework/spree/utils/sort-option-types.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import type { ExpandedProductOption } from '../types'
|
||||||
|
|
||||||
|
const sortOptionsByPosition = (
|
||||||
|
options: ExpandedProductOption[]
|
||||||
|
): ExpandedProductOption[] => {
|
||||||
|
return options.sort((firstOption, secondOption) => {
|
||||||
|
return firstOption.position - secondOption.position
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default sortOptionsByPosition
|
21
framework/spree/utils/tokens/cart-token.ts
Normal file
21
framework/spree/utils/tokens/cart-token.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { requireConfigValue } from '../../isomorphic-config'
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
|
||||||
|
export const getCartToken = () =>
|
||||||
|
Cookies.get(requireConfigValue('cartCookieName') as string)
|
||||||
|
|
||||||
|
export const setCartToken = (cartToken: string) => {
|
||||||
|
const cookieOptions = {
|
||||||
|
expires: requireConfigValue('cartCookieExpire') as number,
|
||||||
|
}
|
||||||
|
|
||||||
|
Cookies.set(
|
||||||
|
requireConfigValue('cartCookieName') as string,
|
||||||
|
cartToken,
|
||||||
|
cookieOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeCartToken = () => {
|
||||||
|
Cookies.remove(requireConfigValue('cartCookieName') as string)
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
import { SpreeSdkResponseWithRawResponse } from '../../types'
|
||||||
|
import type { Client } from '@spree/storefront-api-v2-sdk'
|
||||||
|
import type { IOAuthToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||||
|
import getSpreeSdkMethodFromEndpointPath from '../get-spree-sdk-method-from-endpoint-path'
|
||||||
|
import {
|
||||||
|
ensureUserTokenResponse,
|
||||||
|
removeUserTokenResponse,
|
||||||
|
setUserTokenResponse,
|
||||||
|
} from './user-token-response'
|
||||||
|
import AccessTokenError from '../../errors/AccessTokenError'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the user has a saved access token, make sure it's not expired
|
||||||
|
* If it is expired, attempt to refresh it.
|
||||||
|
*/
|
||||||
|
const ensureFreshUserAccessToken = async (client: Client): Promise<void> => {
|
||||||
|
const userTokenResponse = ensureUserTokenResponse()
|
||||||
|
|
||||||
|
if (!userTokenResponse) {
|
||||||
|
// There's no user token or it has an invalid format.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAccessTokenExpired =
|
||||||
|
(userTokenResponse.created_at + userTokenResponse.expires_in) * 1000 <
|
||||||
|
Date.now()
|
||||||
|
|
||||||
|
if (!isAccessTokenExpired) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const spreeRefreshAccessTokenSdkMethod = getSpreeSdkMethodFromEndpointPath<
|
||||||
|
Client,
|
||||||
|
SpreeSdkResponseWithRawResponse & IOAuthToken
|
||||||
|
>(client, 'authentication.refreshToken')
|
||||||
|
|
||||||
|
const spreeRefreshAccessTokenResponse =
|
||||||
|
await spreeRefreshAccessTokenSdkMethod({
|
||||||
|
refresh_token: userTokenResponse.refresh_token,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (spreeRefreshAccessTokenResponse.isFail()) {
|
||||||
|
removeUserTokenResponse()
|
||||||
|
|
||||||
|
throw new AccessTokenError('Could not refresh access token.')
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserTokenResponse(spreeRefreshAccessTokenResponse.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ensureFreshUserAccessToken
|
25
framework/spree/utils/tokens/ensure-itoken.ts
Normal file
25
framework/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
framework/spree/utils/tokens/is-logged-in.ts
Normal file
9
framework/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
|
49
framework/spree/utils/tokens/revoke-user-tokens.ts
Normal file
49
framework/spree/utils/tokens/revoke-user-tokens.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
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
|
58
framework/spree/utils/tokens/user-token-response.ts
Normal file
58
framework/spree/utils/tokens/user-token-response.ts
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import { requireConfigValue } from '../../isomorphic-config'
|
||||||
|
import Cookies from 'js-cookie'
|
||||||
|
import type { IOAuthToken } from '@spree/storefront-api-v2-sdk/types/interfaces/Token'
|
||||||
|
import UserTokenResponseParseError from '../../errors/UserTokenResponseParseError'
|
||||||
|
|
||||||
|
export const getUserTokenResponse = (): IOAuthToken | undefined => {
|
||||||
|
const stringifiedToken = Cookies.get(
|
||||||
|
requireConfigValue('userCookieName') as string
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!stringifiedToken) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token: IOAuthToken = JSON.parse(stringifiedToken)
|
||||||
|
|
||||||
|
return token
|
||||||
|
} catch (parseError) {
|
||||||
|
throw new UserTokenResponseParseError(
|
||||||
|
'Could not parse stored user token response.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the saved user token response. If the response fails json parsing,
|
||||||
|
* removes the saved token and returns @type {undefined} instead.
|
||||||
|
*/
|
||||||
|
export const ensureUserTokenResponse = (): IOAuthToken | undefined => {
|
||||||
|
try {
|
||||||
|
return getUserTokenResponse()
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof UserTokenResponseParseError) {
|
||||||
|
removeUserTokenResponse()
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setUserTokenResponse = (token: IOAuthToken) => {
|
||||||
|
const cookieOptions = {
|
||||||
|
expires: requireConfigValue('userCookieExpire') as number,
|
||||||
|
}
|
||||||
|
|
||||||
|
Cookies.set(
|
||||||
|
requireConfigValue('userCookieName') as string,
|
||||||
|
JSON.stringify(token),
|
||||||
|
cookieOptions
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeUserTokenResponse = () => {
|
||||||
|
Cookies.remove(requireConfigValue('userCookieName') as string)
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
const validateAllProductsTaxonomyId = (taxonomyId: unknown): string | false => {
|
||||||
|
if (!taxonomyId || taxonomyId === 'false') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof taxonomyId === 'string') {
|
||||||
|
return taxonomyId
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError('taxonomyId must be a string or falsy.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default validateAllProductsTaxonomyId
|
21
framework/spree/utils/validations/validate-cookie-expire.ts
Normal file
21
framework/spree/utils/validations/validate-cookie-expire.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
const validateCookieExpire = (expire: unknown): number => {
|
||||||
|
let expireInteger: number
|
||||||
|
|
||||||
|
if (typeof expire === 'string') {
|
||||||
|
expireInteger = parseFloat(expire)
|
||||||
|
} else if (typeof expire === 'number') {
|
||||||
|
expireInteger = expire
|
||||||
|
} else {
|
||||||
|
throw new TypeError(
|
||||||
|
'expire must be a string containing a number or an integer.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expireInteger < 0) {
|
||||||
|
throw new RangeError('expire must be non-negative.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return expireInteger
|
||||||
|
}
|
||||||
|
|
||||||
|
export default validateCookieExpire
|
@ -0,0 +1,15 @@
|
|||||||
|
const validateImagesOptionFilter = (
|
||||||
|
optionTypeNameOrFalse: unknown
|
||||||
|
): string | false => {
|
||||||
|
if (!optionTypeNameOrFalse || optionTypeNameOrFalse === 'false') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof optionTypeNameOrFalse === 'string') {
|
||||||
|
return optionTypeNameOrFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError('optionTypeNameOrFalse must be a string or falsy.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default validateImagesOptionFilter
|
23
framework/spree/utils/validations/validate-images-quality.ts
Normal file
23
framework/spree/utils/validations/validate-images-quality.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const validateImagesQuality = (quality: unknown): number => {
|
||||||
|
let quality_level: number
|
||||||
|
|
||||||
|
if (typeof quality === 'string') {
|
||||||
|
quality_level = parseInt(quality)
|
||||||
|
} else if (typeof quality === 'number') {
|
||||||
|
quality_level = quality
|
||||||
|
} else {
|
||||||
|
throw new TypeError(
|
||||||
|
'prerenderCount count must be a string containing a number or an integer.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quality_level === NaN) {
|
||||||
|
throw new TypeError(
|
||||||
|
'prerenderCount count must be a string containing a number or an integer.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return quality_level
|
||||||
|
}
|
||||||
|
|
||||||
|
export default validateImagesQuality
|
13
framework/spree/utils/validations/validate-images-size.ts
Normal file
13
framework/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,15 @@
|
|||||||
|
const validatePlaceholderImageUrl = (
|
||||||
|
placeholderUrlOrFalse: unknown
|
||||||
|
): string | false => {
|
||||||
|
if (!placeholderUrlOrFalse || placeholderUrlOrFalse === 'false') {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof placeholderUrlOrFalse === 'string') {
|
||||||
|
return placeholderUrlOrFalse
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError('placeholderUrlOrFalse must be a string or falsy.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export default validatePlaceholderImageUrl
|
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