From 3342d9d1bb7540eef1abd8fc55480dafeb2e4e3d Mon Sep 17 00:00:00 2001
From: Kasper Fabricius Kristensen
<45367945+kasperkristensen@users.noreply.github.com>
Date: Tue, 14 Sep 2021 17:29:38 +0200
Subject: [PATCH] initial provider (#2)
---
framework/commerce/config.js | 3 +
framework/medusa/README.md | 63 +++
framework/medusa/api/endpoints/cart/index.ts | 1 +
.../medusa/api/endpoints/catalog/index.ts | 1 +
.../medusa/api/endpoints/catalog/products.ts | 1 +
.../medusa/api/endpoints/checkout/index.ts | 1 +
.../medusa/api/endpoints/customer/index.ts | 1 +
framework/medusa/api/endpoints/login/index.ts | 1 +
.../medusa/api/endpoints/logout/index.ts | 1 +
.../medusa/api/endpoints/signup/index.ts | 1 +
.../medusa/api/endpoints/wishlist/index.tsx | 1 +
framework/medusa/api/index.ts | 30 ++
.../medusa/api/operations/get-all-pages.ts | 19 +
.../api/operations/get-all-product-paths.ts | 35 ++
.../medusa/api/operations/get-all-products.ts | 41 ++
.../api/operations/get-customer-wishlist.ts | 6 +
framework/medusa/api/operations/get-page.ts | 13 +
.../medusa/api/operations/get-product.ts | 46 ++
.../medusa/api/operations/get-site-info.ts | 33 ++
framework/medusa/api/operations/index.ts | 6 +
.../medusa/api/utils/fetch-medusa-api.ts | 8 +
framework/medusa/api/utils/fetch.ts | 3 +
framework/medusa/auth/index.ts | 3 +
framework/medusa/auth/use-login.tsx | 49 ++
framework/medusa/auth/use-logout.tsx | 29 ++
framework/medusa/auth/use-signup.tsx | 51 +++
framework/medusa/cart/index.ts | 4 +
framework/medusa/cart/use-add-item.tsx | 63 +++
framework/medusa/cart/use-cart.tsx | 76 ++++
framework/medusa/cart/use-remove-item.tsx | 55 +++
framework/medusa/cart/use-update-item.tsx | 91 ++++
framework/medusa/commerce.config.json | 9 +
framework/medusa/const.ts | 2 +
framework/medusa/customer/index.ts | 1 +
framework/medusa/customer/use-customer.tsx | 27 ++
framework/medusa/fetcher.ts | 44 ++
framework/medusa/index.tsx | 9 +
framework/medusa/medusa.ts | 6 +
framework/medusa/next.config.js | 8 +
framework/medusa/product/index.ts | 2 +
framework/medusa/product/use-price.tsx | 2 +
framework/medusa/product/use-search.tsx | 42 ++
framework/medusa/provider.ts | 30 ++
framework/medusa/types/cart.ts | 1 +
framework/medusa/types/checkout.ts | 1 +
framework/medusa/types/common.ts | 1 +
framework/medusa/types/customer.ts | 1 +
framework/medusa/types/index.ts | 25 ++
framework/medusa/types/login.ts | 12 +
framework/medusa/types/logout.ts | 1 +
framework/medusa/types/page.ts | 1 +
framework/medusa/types/product.ts | 1 +
framework/medusa/types/signup.ts | 1 +
framework/medusa/types/site.ts | 1 +
framework/medusa/types/wishlist.ts | 1 +
framework/medusa/utils/call-medusa.ts | 420 ++++++++++++++++++
.../utils/normalizers/normalize-cart.ts | 77 ++++
.../utils/normalizers/normalize-customer.ts | 9 +
.../utils/normalizers/normalize-products.ts | 98 ++++
framework/medusa/wishlist/use-add-item.tsx | 13 +
framework/medusa/wishlist/use-remove-item.tsx | 17 +
framework/medusa/wishlist/use-wishlist.tsx | 43 ++
next.config.js | 1 +
package.json | 1 +
tsconfig.json | 4 +-
yarn.lock | 19 +
66 files changed, 1665 insertions(+), 2 deletions(-)
create mode 100644 framework/medusa/README.md
create mode 100644 framework/medusa/api/endpoints/cart/index.ts
create mode 100644 framework/medusa/api/endpoints/catalog/index.ts
create mode 100644 framework/medusa/api/endpoints/catalog/products.ts
create mode 100644 framework/medusa/api/endpoints/checkout/index.ts
create mode 100644 framework/medusa/api/endpoints/customer/index.ts
create mode 100644 framework/medusa/api/endpoints/login/index.ts
create mode 100644 framework/medusa/api/endpoints/logout/index.ts
create mode 100644 framework/medusa/api/endpoints/signup/index.ts
create mode 100644 framework/medusa/api/endpoints/wishlist/index.tsx
create mode 100644 framework/medusa/api/index.ts
create mode 100644 framework/medusa/api/operations/get-all-pages.ts
create mode 100644 framework/medusa/api/operations/get-all-product-paths.ts
create mode 100644 framework/medusa/api/operations/get-all-products.ts
create mode 100644 framework/medusa/api/operations/get-customer-wishlist.ts
create mode 100644 framework/medusa/api/operations/get-page.ts
create mode 100644 framework/medusa/api/operations/get-product.ts
create mode 100644 framework/medusa/api/operations/get-site-info.ts
create mode 100644 framework/medusa/api/operations/index.ts
create mode 100644 framework/medusa/api/utils/fetch-medusa-api.ts
create mode 100644 framework/medusa/api/utils/fetch.ts
create mode 100644 framework/medusa/auth/index.ts
create mode 100644 framework/medusa/auth/use-login.tsx
create mode 100644 framework/medusa/auth/use-logout.tsx
create mode 100644 framework/medusa/auth/use-signup.tsx
create mode 100644 framework/medusa/cart/index.ts
create mode 100644 framework/medusa/cart/use-add-item.tsx
create mode 100644 framework/medusa/cart/use-cart.tsx
create mode 100644 framework/medusa/cart/use-remove-item.tsx
create mode 100644 framework/medusa/cart/use-update-item.tsx
create mode 100644 framework/medusa/commerce.config.json
create mode 100644 framework/medusa/const.ts
create mode 100644 framework/medusa/customer/index.ts
create mode 100644 framework/medusa/customer/use-customer.tsx
create mode 100644 framework/medusa/fetcher.ts
create mode 100644 framework/medusa/index.tsx
create mode 100644 framework/medusa/medusa.ts
create mode 100644 framework/medusa/next.config.js
create mode 100644 framework/medusa/product/index.ts
create mode 100644 framework/medusa/product/use-price.tsx
create mode 100644 framework/medusa/product/use-search.tsx
create mode 100644 framework/medusa/provider.ts
create mode 100644 framework/medusa/types/cart.ts
create mode 100644 framework/medusa/types/checkout.ts
create mode 100644 framework/medusa/types/common.ts
create mode 100644 framework/medusa/types/customer.ts
create mode 100644 framework/medusa/types/index.ts
create mode 100644 framework/medusa/types/login.ts
create mode 100644 framework/medusa/types/logout.ts
create mode 100644 framework/medusa/types/page.ts
create mode 100644 framework/medusa/types/product.ts
create mode 100644 framework/medusa/types/signup.ts
create mode 100644 framework/medusa/types/site.ts
create mode 100644 framework/medusa/types/wishlist.ts
create mode 100644 framework/medusa/utils/call-medusa.ts
create mode 100644 framework/medusa/utils/normalizers/normalize-cart.ts
create mode 100644 framework/medusa/utils/normalizers/normalize-customer.ts
create mode 100644 framework/medusa/utils/normalizers/normalize-products.ts
create mode 100644 framework/medusa/wishlist/use-add-item.tsx
create mode 100644 framework/medusa/wishlist/use-remove-item.tsx
create mode 100644 framework/medusa/wishlist/use-wishlist.tsx
diff --git a/framework/commerce/config.js b/framework/commerce/config.js
index 019c59a51..04248d2f9 100644
--- a/framework/commerce/config.js
+++ b/framework/commerce/config.js
@@ -14,6 +14,7 @@ const PROVIDERS = [
'shopify',
'swell',
'vendure',
+ 'medusa',
]
function getProviderName() {
@@ -25,6 +26,8 @@ function getProviderName() {
? 'shopify'
: process.env.NEXT_PUBLIC_SWELL_STORE_ID
? 'swell'
+ : process.env.NEXT_PUBLIC_MEDUSA_STORE_URL
+ ? 'medusa'
: 'local')
)
}
diff --git a/framework/medusa/README.md b/framework/medusa/README.md
new file mode 100644
index 000000000..8cc7051bc
--- /dev/null
+++ b/framework/medusa/README.md
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+ Medusa Provider
+
+
+Medusa is an open-source headless commerce engine that enables developers to create amazing digital commerce experiences.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Demo
+
+You can view a working demo of the Medusa provider for Next.js Commerce at https://medusa.vercel.store/
+
+## Quickstart
+
+You need a [Medusa](https://medusa-commerce.com/) instance, either in the cloud or self-hosted.
+
+Clone this repo and install dependencies using `yarn` or `npm install`
+
+Copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
+
+```bash
+mv framework/medusa/.env.template .env.local
+```
+
+Then, set the environment following variables in your `.env.local`.
+
+- `NEXT_PUBLIC_MEDUSA_STORE_URL` must point to the URL where your Medusa instance is running.
+
+- `NEXT_PUBLIC_MEDUSA_IMAGE_HOST` must point to your image hosting service.
+
+```
+COMMERCE_PROVIDER=medusa
+NEXT_PUBLIC_MEDUSA_STORE_URL=https://medusa-demo.store
+NEXT_PUBLIC_MEDUSA_IMG_HOST=medusa-public-images.s3.eu-west-1.amazonaws.com
+```
+
+## Notes
+
+- The entire customer flow is carried out using the [Storefront API](https://docs.medusa-commerce.com/api/store). This means that there is no existing, pre-built checkout flow. The checkout flow must be built using the `Storefront API`, for an example of how to do this feel free to have a look at our [Next.js](https://github.com/medusajs/gatsby-starter-medusa) starter project.
+
+- `Medusa` does not currently support any wishlist features.
+
+- `Medusa` does not nativly support searches. This can be implemented using plugins such as `MeiliSearch`, see [#381](https://github.com/medusajs/medusa/pull/381).
+
+- `Medusa` does not come with any page/blog building feature. This can be implemented using `Medusa` in conjunction with a CMS such as `Contentful`. For inspiration on how to do this check out our [Contentful starter](https://github.com/medusajs/medusa-starter-contentful)
diff --git a/framework/medusa/api/endpoints/cart/index.ts b/framework/medusa/api/endpoints/cart/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/medusa/api/endpoints/cart/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/medusa/api/endpoints/catalog/index.ts b/framework/medusa/api/endpoints/catalog/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/medusa/api/endpoints/catalog/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/medusa/api/endpoints/catalog/products.ts b/framework/medusa/api/endpoints/catalog/products.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/medusa/api/endpoints/catalog/products.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/medusa/api/endpoints/checkout/index.ts b/framework/medusa/api/endpoints/checkout/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/medusa/api/endpoints/checkout/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/medusa/api/endpoints/customer/index.ts b/framework/medusa/api/endpoints/customer/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/medusa/api/endpoints/customer/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/medusa/api/endpoints/login/index.ts b/framework/medusa/api/endpoints/login/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/medusa/api/endpoints/login/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/medusa/api/endpoints/logout/index.ts b/framework/medusa/api/endpoints/logout/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/medusa/api/endpoints/logout/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/medusa/api/endpoints/signup/index.ts b/framework/medusa/api/endpoints/signup/index.ts
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/medusa/api/endpoints/signup/index.ts
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/medusa/api/endpoints/wishlist/index.tsx b/framework/medusa/api/endpoints/wishlist/index.tsx
new file mode 100644
index 000000000..491bf0ac9
--- /dev/null
+++ b/framework/medusa/api/endpoints/wishlist/index.tsx
@@ -0,0 +1 @@
+export default function noopApi(...args: any[]): void {}
diff --git a/framework/medusa/api/index.ts b/framework/medusa/api/index.ts
new file mode 100644
index 000000000..c93e4843b
--- /dev/null
+++ b/framework/medusa/api/index.ts
@@ -0,0 +1,30 @@
+import type { CommerceAPIConfig } from '@commerce/api'
+import { CommerceAPI, getCommerceApi as commerceApi } from '@commerce/api'
+import fetchApi from './utils/fetch-medusa-api'
+import { MEDUSA_CART_ID_COOKIE } from '../const'
+
+import * as operations from './operations'
+
+export interface MedusaConfig extends CommerceAPIConfig {
+ fetch: any
+}
+
+const config: MedusaConfig = {
+ commerceUrl: '',
+ apiToken: '',
+ cartCookie: MEDUSA_CART_ID_COOKIE,
+ customerCookie: '',
+ cartCookieMaxAge: 60 * 60 * 24 * 30,
+ fetch: fetchApi,
+}
+
+export const provider = { config, operations }
+
+export type Provider = typeof provider
+export type MedusaAPI = CommerceAPI
+
+export function getCommerceApi
(
+ customProvider: P = provider as any
+): MedusaAPI
{
+ return commerceApi(customProvider as any)
+}
diff --git a/framework/medusa/api/operations/get-all-pages.ts b/framework/medusa/api/operations/get-all-pages.ts
new file mode 100644
index 000000000..ffcab589f
--- /dev/null
+++ b/framework/medusa/api/operations/get-all-pages.ts
@@ -0,0 +1,19 @@
+export type Page = { url: string }
+export type GetAllPagesResult = { pages: Page[] }
+import type { MedusaConfig } from '..'
+
+export default function getAllPagesOperation() {
+ function getAllPages({
+ config,
+ preview,
+ }: {
+ url?: string
+ config?: Partial
+ preview?: boolean
+ }): Promise {
+ return Promise.resolve({
+ pages: [],
+ })
+ }
+ return getAllPages
+}
diff --git a/framework/medusa/api/operations/get-all-product-paths.ts b/framework/medusa/api/operations/get-all-product-paths.ts
new file mode 100644
index 000000000..716839bba
--- /dev/null
+++ b/framework/medusa/api/operations/get-all-product-paths.ts
@@ -0,0 +1,35 @@
+import { OperationContext } from '@commerce/api/operations'
+import { Product } from '@commerce/types/product'
+import { Product as MedusaProduct } from '@medusajs/medusa-js/lib/types'
+import { MedusaConfig } from '..'
+
+export type GetAllProductPathsResult = {
+ products: Array<{ path: string }>
+}
+
+export default function getAllProductPathsOperation({
+ commerce,
+}: OperationContext) {
+ async function getAllProductsPaths({
+ config: cfg,
+ }: {
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise<{ products: Product[] | any[] }> {
+ const config = commerce.getConfig(cfg)
+
+ const results = await config.fetch('products', 'list', {})
+
+ const productHandles = results.data?.products
+ ? results.data.products.map(({ handle }: MedusaProduct) => ({
+ path: `/${handle}`,
+ }))
+ : []
+
+ return {
+ products: productHandles,
+ }
+ }
+
+ return getAllProductsPaths
+}
diff --git a/framework/medusa/api/operations/get-all-products.ts b/framework/medusa/api/operations/get-all-products.ts
new file mode 100644
index 000000000..e6bb0529b
--- /dev/null
+++ b/framework/medusa/api/operations/get-all-products.ts
@@ -0,0 +1,41 @@
+import { Product } from '@commerce/types/product'
+import type { OperationContext } from '@commerce/api/operations'
+import type { MedusaConfig } from '../'
+import { normalizeProduct } from '@framework/utils/normalizers/normalize-products'
+import { Product as MedusaProduct } from '@medusajs/medusa-js/lib/types'
+
+export type ProductVariables = { first?: number }
+
+export default function getAllProductsOperation({
+ commerce,
+}: OperationContext) {
+ async function getAllProducts({
+ config: cfg,
+ variables,
+ }: {
+ query?: string
+ variables?: ProductVariables
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise<{ products: Product[] | any[] }> {
+ const config = commerce.getConfig(cfg)
+ const query = variables?.first && { limit: variables.first }
+
+ const results = await config.fetch(
+ 'products',
+ 'list',
+ query ? { query: query } : {}
+ )
+
+ const products: Product[] = results.data?.products
+ ? results.data.products.map((product: MedusaProduct) =>
+ normalizeProduct(product)
+ )
+ : []
+
+ return {
+ products,
+ }
+ }
+ return getAllProducts
+}
diff --git a/framework/medusa/api/operations/get-customer-wishlist.ts b/framework/medusa/api/operations/get-customer-wishlist.ts
new file mode 100644
index 000000000..8c34b9e87
--- /dev/null
+++ b/framework/medusa/api/operations/get-customer-wishlist.ts
@@ -0,0 +1,6 @@
+export default function getCustomerWishlistOperation() {
+ function getCustomerWishlist(): any {
+ return { wishlist: {} }
+ }
+ return getCustomerWishlist
+}
diff --git a/framework/medusa/api/operations/get-page.ts b/framework/medusa/api/operations/get-page.ts
new file mode 100644
index 000000000..b0cfdf58f
--- /dev/null
+++ b/framework/medusa/api/operations/get-page.ts
@@ -0,0 +1,13 @@
+export type Page = any
+export type GetPageResult = { page?: Page }
+
+export type PageVariables = {
+ id: number
+}
+
+export default function getPageOperation() {
+ function getPage(): Promise {
+ return Promise.resolve({})
+ }
+ return getPage
+}
diff --git a/framework/medusa/api/operations/get-product.ts b/framework/medusa/api/operations/get-product.ts
new file mode 100644
index 000000000..1cece8a96
--- /dev/null
+++ b/framework/medusa/api/operations/get-product.ts
@@ -0,0 +1,46 @@
+import { Product } from '@commerce/types/product'
+import { GetProductOperation } from '@commerce/types/product'
+import type { OperationContext } from '@commerce/api/operations'
+import { Product as MedusaProduct } from '@medusajs/medusa-js/lib/types'
+import { normalizeProduct } from '@framework/utils/normalizers/normalize-products'
+import { MedusaConfig } from '..'
+
+export default function getProductOperation({
+ commerce,
+}: OperationContext) {
+ async function getProduct({
+ variables,
+ config: cfg,
+ }: {
+ query?: string
+ variables?: T['variables']
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ const config = commerce.getConfig(cfg)
+
+ const response = await config.fetch('products', 'list', {})
+
+ if (response.data?.products) {
+ const products: MedusaProduct[] = response.data.products
+ const product = products
+ ? products.find(({ handle }) => handle === variables!.slug)
+ : null
+
+ /**
+ * Commerce only provides us with the slug/path for finding
+ * the specified product. We do not have a endpoint for retrieving
+ * products using handles. Perhaps we should ask Vercel if we can
+ * change this so the variables also expose the product_id, which
+ * we can use for retrieving products.
+ */
+ if (product) {
+ return {
+ product: normalizeProduct(product),
+ }
+ }
+ }
+ }
+
+ return getProduct
+}
diff --git a/framework/medusa/api/operations/get-site-info.ts b/framework/medusa/api/operations/get-site-info.ts
new file mode 100644
index 000000000..a914069be
--- /dev/null
+++ b/framework/medusa/api/operations/get-site-info.ts
@@ -0,0 +1,33 @@
+import { OperationContext } from '@commerce/api/operations'
+import { Category } from '@commerce/types/site'
+import { MedusaConfig } from '..'
+
+export type GetSiteInfoResult<
+ T extends { categories: any[]; brands: any[] } = {
+ categories: Category[]
+ brands: any[]
+ }
+> = T
+
+export default function getSiteInfoOperation({}: OperationContext) {
+ function getSiteInfo({
+ query,
+ variables,
+ config: cfg,
+ }: {
+ query?: string
+ variables?: any
+ config?: Partial
+ preview?: boolean
+ } = {}): Promise {
+ /** We should add collections to our Storefront API,
+ * so we can populate the site with collections here
+ */
+ return Promise.resolve({
+ categories: [],
+ brands: [],
+ })
+ }
+
+ return getSiteInfo
+}
diff --git a/framework/medusa/api/operations/index.ts b/framework/medusa/api/operations/index.ts
new file mode 100644
index 000000000..086fdf83a
--- /dev/null
+++ b/framework/medusa/api/operations/index.ts
@@ -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'
diff --git a/framework/medusa/api/utils/fetch-medusa-api.ts b/framework/medusa/api/utils/fetch-medusa-api.ts
new file mode 100644
index 000000000..da78fba60
--- /dev/null
+++ b/framework/medusa/api/utils/fetch-medusa-api.ts
@@ -0,0 +1,8 @@
+import { callMedusa } from '@framework/utils/call-medusa'
+
+const fetchApi = async (query: string, method: string, variables: any) => {
+ const response = await callMedusa(method, query, variables)
+
+ return response
+}
+export default fetchApi
diff --git a/framework/medusa/api/utils/fetch.ts b/framework/medusa/api/utils/fetch.ts
new file mode 100644
index 000000000..9d9fff3ed
--- /dev/null
+++ b/framework/medusa/api/utils/fetch.ts
@@ -0,0 +1,3 @@
+import zeitFetch from '@vercel/fetch'
+
+export default zeitFetch()
diff --git a/framework/medusa/auth/index.ts b/framework/medusa/auth/index.ts
new file mode 100644
index 000000000..36e757a89
--- /dev/null
+++ b/framework/medusa/auth/index.ts
@@ -0,0 +1,3 @@
+export { default as useLogin } from './use-login'
+export { default as useLogout } from './use-logout'
+export { default as useSignup } from './use-signup'
diff --git a/framework/medusa/auth/use-login.tsx b/framework/medusa/auth/use-login.tsx
new file mode 100644
index 000000000..39673392d
--- /dev/null
+++ b/framework/medusa/auth/use-login.tsx
@@ -0,0 +1,49 @@
+import { MutationHook } from '@commerce/utils/types'
+import useLogin, { UseLogin } from '@commerce/auth/use-login'
+import { CommerceError, ValidationError } from '@commerce/utils/errors'
+import { useCustomer } from '../customer'
+import { useCallback } from 'react'
+
+export default useLogin as UseLogin
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: 'auth',
+ method: 'authenticate',
+ },
+ async fetcher({ input: { email, password }, options, fetch }) {
+ if (!(email && password)) {
+ throw new CommerceError({
+ message: 'An email and password are required to login',
+ })
+ }
+
+ await fetch({
+ ...options,
+ variables: { email: email, password: password },
+ }).catch((_e) => {
+ throw new CommerceError({
+ errors: [
+ new ValidationError({
+ message:
+ 'A user with that email and password combination does not exist',
+ }),
+ ],
+ })
+ })
+ },
+ useHook:
+ ({ fetch }) =>
+ () => {
+ const { revalidate } = useCustomer()
+
+ return useCallback(
+ async function login(input) {
+ const data = await fetch({ input })
+ await revalidate()
+ return data
+ },
+ [fetch, revalidate]
+ )
+ },
+}
diff --git a/framework/medusa/auth/use-logout.tsx b/framework/medusa/auth/use-logout.tsx
new file mode 100644
index 000000000..6d59ed308
--- /dev/null
+++ b/framework/medusa/auth/use-logout.tsx
@@ -0,0 +1,29 @@
+import { MutationHook } from '@commerce/utils/types'
+import useLogout, { UseLogout } from '@commerce/auth/use-logout'
+import { useCallback } from 'react'
+import Cookies from 'js-cookie'
+
+export default useLogout as UseLogout
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: 'auth',
+ method: 'logout',
+ },
+ async fetcher({ options, fetch }) {
+ await fetch({ ...options })
+
+ return null
+ },
+ useHook:
+ ({ fetch }) =>
+ () => {
+ return useCallback(
+ async function logout(input) {
+ const data = await fetch({ input })
+ return data
+ },
+ [fetch]
+ )
+ },
+}
diff --git a/framework/medusa/auth/use-signup.tsx b/framework/medusa/auth/use-signup.tsx
new file mode 100644
index 000000000..0d996f57f
--- /dev/null
+++ b/framework/medusa/auth/use-signup.tsx
@@ -0,0 +1,51 @@
+import { useCallback } from 'react'
+import useCustomer from '../customer/use-customer'
+import { MutationHook } from '@commerce/utils/types'
+import useSignup, { UseSignup } from '@commerce/auth/use-signup'
+import { CommerceError } from '@commerce/utils/errors'
+
+export default useSignup as UseSignup
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: 'customers',
+ method: 'create',
+ },
+ async fetcher({
+ input: { firstName, lastName, email, password },
+ options,
+ fetch,
+ }) {
+ if (!(firstName && lastName && email && password)) {
+ throw new CommerceError({
+ message:
+ 'A first name, last name, email and password are required to signup',
+ })
+ }
+ return await fetch({
+ ...options,
+ variables: {
+ payload: {
+ first_name: firstName,
+ last_name: lastName,
+ email,
+ password,
+ },
+ },
+ })
+ },
+ useHook:
+ ({ fetch }) =>
+ () => {
+ const { revalidate } = useCustomer()
+
+ return useCallback(
+ async function signup(input) {
+ const data = await fetch({ input })
+ await revalidate()
+ return data
+ },
+ [fetch, revalidate]
+ )
+ },
+}
diff --git a/framework/medusa/cart/index.ts b/framework/medusa/cart/index.ts
new file mode 100644
index 000000000..3b8ba990e
--- /dev/null
+++ b/framework/medusa/cart/index.ts
@@ -0,0 +1,4 @@
+export { default as useCart } from './use-cart'
+export { default as useAddItem } from './use-add-item'
+export { default as useRemoveItem } from './use-remove-item'
+export { default as useUpdateItem } from './use-update-item'
diff --git a/framework/medusa/cart/use-add-item.tsx b/framework/medusa/cart/use-add-item.tsx
new file mode 100644
index 000000000..1f68e81e2
--- /dev/null
+++ b/framework/medusa/cart/use-add-item.tsx
@@ -0,0 +1,63 @@
+import useAddItem, { UseAddItem } from '@commerce/cart/use-add-item'
+import { CommerceError } from '@commerce/utils/errors'
+import { MutationHook } from '@commerce/utils/types'
+import { MEDUSA_CART_ID_COOKIE } from '@framework/const'
+import { MedusaAddItemProps } from '@framework/types'
+import type { AddItemHook } from '../types/cart'
+import { normalizeCart } from '@framework/utils/normalizers/normalize-cart'
+import { useCart } from 'framework/local/cart'
+import Cookies from 'js-cookie'
+import { useCallback } from 'react'
+
+export default useAddItem as UseAddItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: 'carts',
+ method: 'addItem',
+ },
+ async fetcher({ input: item, options, fetch }) {
+ if (item.quantity && !Number.isInteger(item.quantity)) {
+ throw new CommerceError({
+ message: 'The item quantity has to be a valid integer greater than 0',
+ })
+ }
+
+ const variables: {
+ cart_id: string
+ payload: MedusaAddItemProps
+ } = {
+ cart_id: Cookies.get(MEDUSA_CART_ID_COOKIE)!,
+ payload: {
+ variant_id: item.variantId,
+ quantity: item.quantity ?? 1,
+ },
+ }
+
+ try {
+ const data = await fetch({
+ ...options,
+ variables,
+ })
+
+ return normalizeCart(data.cart)
+ } catch (e: any) {
+ console.log(e)
+ throw new CommerceError({ message: e.message })
+ }
+ },
+ useHook:
+ ({ fetch }) =>
+ () => {
+ const { mutate } = useCart()
+
+ return useCallback(
+ async function addItem(input) {
+ const data = await fetch({ input })
+ await mutate(data, false)
+ return data
+ },
+ [fetch, mutate]
+ )
+ },
+}
diff --git a/framework/medusa/cart/use-cart.tsx b/framework/medusa/cart/use-cart.tsx
new file mode 100644
index 000000000..86db4b137
--- /dev/null
+++ b/framework/medusa/cart/use-cart.tsx
@@ -0,0 +1,76 @@
+import { useMemo } from 'react'
+import { SWRHook } from '@commerce/utils/types'
+import useCart, { UseCart } from '@commerce/cart/use-cart'
+import { normalizeCart } from '@framework/utils/normalizers/normalize-cart'
+import { CommerceError } from '@commerce/utils/errors'
+
+import Cookies from 'js-cookie'
+import { MEDUSA_CART_ID_COOKIE } from '@framework/const'
+
+export default useCart as UseCart
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: '',
+ },
+ async fetcher({ fetch }) {
+ const cart_id = Cookies.get(MEDUSA_CART_ID_COOKIE)
+
+ /**
+ * If cart already exits, then try to fetch it
+ */
+ if (cart_id) {
+ try {
+ const existingCartResponse = await fetch({
+ query: 'carts',
+ method: 'retrieve',
+ variables: { cart_id: cart_id },
+ })
+
+ if (existingCartResponse?.cart) {
+ return normalizeCart(existingCartResponse.cart)
+ }
+ } catch (e) {
+ /**
+ * noop: If the cart_id does not exits, then we
+ * continue and create a new cart and set a new
+ * CART_COOKIE
+ */
+ }
+ }
+
+ const newCartResponse = await fetch({
+ query: 'carts',
+ method: 'create',
+ variables: {},
+ })
+
+ if (newCartResponse?.cart) {
+ Cookies.set(MEDUSA_CART_ID_COOKIE, newCartResponse.cart.id, {
+ expires: 30,
+ })
+ return normalizeCart(newCartResponse.cart)
+ }
+ throw new CommerceError({ message: 'Medusa cart error' })
+ },
+ useHook:
+ ({ useData }) =>
+ (input) => {
+ const response = useData({
+ swrOptions: { revalidateOnFocus: false, ...input?.swrOptions },
+ })
+
+ return useMemo(
+ () =>
+ Object.create(response, {
+ isEmpty: {
+ get() {
+ return (response.data?.lineItems.length ?? 0) <= 0
+ },
+ enumerable: true,
+ },
+ }),
+ [response]
+ )
+ },
+}
diff --git a/framework/medusa/cart/use-remove-item.tsx b/framework/medusa/cart/use-remove-item.tsx
new file mode 100644
index 000000000..29a7365cf
--- /dev/null
+++ b/framework/medusa/cart/use-remove-item.tsx
@@ -0,0 +1,55 @@
+import {
+ HookFetcherContext,
+ MutationHook,
+ MutationHookContext,
+} from '@commerce/utils/types'
+import useRemoveItem, { UseRemoveItem } from '@commerce/cart/use-remove-item'
+import { MEDUSA_CART_ID_COOKIE } from '@framework/const'
+import Cookies from 'js-cookie'
+import { normalizeCart } from '@framework/utils/normalizers/normalize-cart'
+import { CommerceError } from '@commerce/utils/errors'
+import { RemoveItemHook } from '@commerce/types/cart'
+import { useCallback } from 'react'
+import useCart from './use-cart'
+
+export default useRemoveItem as UseRemoveItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: 'carts',
+ method: 'deleteItem',
+ },
+ async fetcher({
+ input: { itemId },
+ options,
+ fetch,
+ }: HookFetcherContext) {
+ const cart_id = Cookies.get(MEDUSA_CART_ID_COOKIE)
+
+ if (cart_id) {
+ const data = await fetch({
+ ...options,
+ variables: { cart_id: cart_id, line_id: itemId },
+ })
+
+ return normalizeCart(data.cart)
+ } else {
+ throw new CommerceError({ message: 'No cart was found' })
+ }
+ },
+ useHook:
+ ({ fetch }: MutationHookContext) =>
+ () => {
+ const { mutate } = useCart()
+
+ return useCallback(
+ async function removeItem(input) {
+ const data = await fetch({ input: { itemId: input.id } })
+ await mutate(data, false)
+
+ return data
+ },
+ [fetch, mutate]
+ )
+ },
+}
diff --git a/framework/medusa/cart/use-update-item.tsx b/framework/medusa/cart/use-update-item.tsx
new file mode 100644
index 000000000..525f1bd8e
--- /dev/null
+++ b/framework/medusa/cart/use-update-item.tsx
@@ -0,0 +1,91 @@
+import { MutationHook, MutationHookContext } from '@commerce/utils/types'
+import useUpdateItem, { UseUpdateItem } from '@commerce/cart/use-update-item'
+import { handler as removeItem } from './use-remove-item'
+import { CommerceError, ValidationError } from '@commerce/utils/errors'
+import Cookies from 'js-cookie'
+import { MEDUSA_CART_ID_COOKIE } from '@framework/const'
+import { normalizeCart } from '@framework/utils/normalizers/normalize-cart'
+import { LineItem, UpdateItemHook } from '@commerce/types/cart'
+import { useCallback } from 'react'
+import { debounce } from 'lodash'
+import useCart from '@commerce/cart/use-cart'
+
+export type UpdateItemActionInput = T extends LineItem
+ ? Partial
+ : UpdateItemHook['actionInput']
+
+export default useUpdateItem as UseUpdateItem
+
+export const handler: MutationHook = {
+ fetchOptions: {
+ query: 'carts',
+ method: 'updateItem',
+ },
+ async fetcher({ input: { itemId, item }, options, fetch }) {
+ if (Number.isInteger(item.quantity)) {
+ if (item.quantity! < 1) {
+ return removeItem.fetcher!({
+ options: removeItem.fetchOptions,
+ input: { itemId },
+ fetch,
+ })
+ }
+ } else if (item.quantity) {
+ throw new ValidationError({
+ message: 'The item quantity has to be a valid integer',
+ })
+ }
+
+ const cart_id = Cookies.get(MEDUSA_CART_ID_COOKIE)
+
+ const data = await fetch({
+ ...options,
+ variables: {
+ cart_id: cart_id,
+ line_id: itemId,
+ payload: { quantity: item.quantity },
+ },
+ })
+
+ if (data.cart) {
+ return normalizeCart(data.cart)
+ } else {
+ throw new CommerceError({ message: 'No cart was found' })
+ }
+ },
+ useHook:
+ ({ fetch }: MutationHookContext) =>
+ (
+ ctx: {
+ item?: T
+ wait?: number
+ } = {}
+ ) => {
+ const { item } = ctx
+ const { mutate } = useCart()
+
+ return useCallback(
+ debounce(async (input: UpdateItemActionInput) => {
+ const itemId = input.id ?? item?.id
+ const productId = input.productId ?? item?.productId
+ const variantId = input.productId ?? item?.variantId
+
+ if (!itemId || !productId || !variantId) {
+ throw new ValidationError({
+ message: 'Invalid input used for this operation',
+ })
+ }
+
+ const data = await fetch({
+ input: {
+ itemId,
+ item: { productId, variantId, quantity: input.quantity },
+ },
+ })
+ await mutate(data, false)
+ return data
+ }, ctx.wait ?? 500),
+ [fetch, mutate]
+ )
+ },
+}
diff --git a/framework/medusa/commerce.config.json b/framework/medusa/commerce.config.json
new file mode 100644
index 000000000..8aac22c87
--- /dev/null
+++ b/framework/medusa/commerce.config.json
@@ -0,0 +1,9 @@
+{
+ "provider": "medusa",
+ "features": {
+ "wishlist": false,
+ "customerAuth": true,
+ "customCheckout": true,
+ "search": false
+ }
+}
diff --git a/framework/medusa/const.ts b/framework/medusa/const.ts
new file mode 100644
index 000000000..81acb4b7e
--- /dev/null
+++ b/framework/medusa/const.ts
@@ -0,0 +1,2 @@
+export const MEDUSA_PUBLIC_STORE_URL = process.env.NEXT_PUBLIC_MEDUSA_STORE_URL
+export const MEDUSA_CART_ID_COOKIE = 'medusa_cart_id'
diff --git a/framework/medusa/customer/index.ts b/framework/medusa/customer/index.ts
new file mode 100644
index 000000000..6c903ecc5
--- /dev/null
+++ b/framework/medusa/customer/index.ts
@@ -0,0 +1 @@
+export { default as useCustomer } from './use-customer'
diff --git a/framework/medusa/customer/use-customer.tsx b/framework/medusa/customer/use-customer.tsx
new file mode 100644
index 000000000..649e7527a
--- /dev/null
+++ b/framework/medusa/customer/use-customer.tsx
@@ -0,0 +1,27 @@
+import { SWRHook } from '@commerce/utils/types'
+import useCustomer, { UseCustomer } from '@commerce/customer/use-customer'
+import { normalizeCustomer } from '@framework/utils/normalizers/normalize-customer'
+
+export default useCustomer as UseCustomer
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: 'auth',
+ method: 'getSession',
+ },
+ async fetcher({ options, fetch }) {
+ const data = await fetch({
+ ...options,
+ })
+ return normalizeCustomer(data?.customer) || null
+ },
+ useHook:
+ ({ useData }) =>
+ (input) => {
+ return useData({
+ swrOptions: {
+ revalidateOnFocus: false,
+ ...input?.swrOptions,
+ },
+ })
+ },
+}
diff --git a/framework/medusa/fetcher.ts b/framework/medusa/fetcher.ts
new file mode 100644
index 000000000..8dfceb91f
--- /dev/null
+++ b/framework/medusa/fetcher.ts
@@ -0,0 +1,44 @@
+import { CommerceError } from '@commerce/utils/errors'
+import { Fetcher } from '@commerce/utils/types'
+import { callMedusa } from './utils/call-medusa'
+
+enum Query {
+ Auth = 'auth',
+ Carts = 'carts',
+ Customers = 'customers',
+ Errors = 'errors',
+ Orders = 'orders',
+ Products = 'products',
+ ReturnReasons = 'returnReasons',
+ Returns = 'returns',
+ ShippingOptions = 'shippingOptions',
+ Swaps = 'swaps',
+}
+
+export const fetcher: Fetcher = async ({ method, query, variables }) => {
+ if (!query) {
+ throw new CommerceError({ message: 'An argument for query is required' })
+ }
+
+ if (!Object.values(Query).includes(query!)) {
+ throw new CommerceError({
+ message: `${query} is not a valid method argument. Available queries are ${Object.keys(
+ Query
+ )
+ .map((k) => Query[k as any])
+ .join(', ')}`,
+ })
+ }
+
+ if (!method) {
+ throw new CommerceError({ message: 'An argument for method is required' })
+ }
+
+ const response = await callMedusa(method, query, variables)
+
+ if (response.statusText === 'OK') {
+ const { data } = response
+ return data
+ }
+ throw response
+}
diff --git a/framework/medusa/index.tsx b/framework/medusa/index.tsx
new file mode 100644
index 000000000..56c28b0ac
--- /dev/null
+++ b/framework/medusa/index.tsx
@@ -0,0 +1,9 @@
+import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
+import { medusaProvider, MedusaProvider } from './provider'
+
+export { medusaProvider }
+export type { MedusaProvider }
+
+export const CommerceProvider = getCommerceProvider(medusaProvider)
+
+export const useCommerce = () => useCoreCommerce()
diff --git a/framework/medusa/medusa.ts b/framework/medusa/medusa.ts
new file mode 100644
index 000000000..45a5ac945
--- /dev/null
+++ b/framework/medusa/medusa.ts
@@ -0,0 +1,6 @@
+import Medusa from '@medusajs/medusa-js'
+import { MEDUSA_PUBLIC_STORE_URL } from './const'
+
+const medusa: Medusa = new Medusa({ baseUrl: MEDUSA_PUBLIC_STORE_URL! })
+
+export default medusa
diff --git a/framework/medusa/next.config.js b/framework/medusa/next.config.js
new file mode 100644
index 000000000..558a8228d
--- /dev/null
+++ b/framework/medusa/next.config.js
@@ -0,0 +1,8 @@
+const commerce = require('./commerce.config.json')
+
+module.exports = {
+ commerce,
+ images: {
+ domains: [process.env.NEXT_PUBLIC_MEDUSA_IMG_HOST],
+ },
+}
diff --git a/framework/medusa/product/index.ts b/framework/medusa/product/index.ts
new file mode 100644
index 000000000..426a3edcd
--- /dev/null
+++ b/framework/medusa/product/index.ts
@@ -0,0 +1,2 @@
+export { default as usePrice } from './use-price'
+export { default as useSearch } from './use-search'
diff --git a/framework/medusa/product/use-price.tsx b/framework/medusa/product/use-price.tsx
new file mode 100644
index 000000000..0174faf5e
--- /dev/null
+++ b/framework/medusa/product/use-price.tsx
@@ -0,0 +1,2 @@
+export * from '@commerce/product/use-price'
+export { default } from '@commerce/product/use-price'
diff --git a/framework/medusa/product/use-search.tsx b/framework/medusa/product/use-search.tsx
new file mode 100644
index 000000000..d6c613000
--- /dev/null
+++ b/framework/medusa/product/use-search.tsx
@@ -0,0 +1,42 @@
+import { SWRHook } from '@commerce/utils/types'
+import useSearch, { UseSearch } from '@commerce/product/use-search'
+import { Product } from '@commerce/types/product'
+import { Product as MedusaProduct } from '@medusajs/medusa-js/lib/types'
+import { normalizeProduct } from '@framework/utils/normalizers/normalize-products'
+export default useSearch as UseSearch
+
+export const handler: SWRHook = {
+ fetchOptions: {
+ query: 'products',
+ method: 'list',
+ },
+ async fetcher({ input, options, fetch }) {
+ const { products } = await fetch({
+ ...options,
+ variables: { query: null },
+ })
+
+ return {
+ products: products
+ ? products.map((product: MedusaProduct) => normalizeProduct(product))
+ : [],
+ found: products.length,
+ }
+ },
+ useHook:
+ ({ useData }) =>
+ ({ input = {} }) => {
+ return useData({
+ input: [
+ ['search', input.search],
+ ['categoryId', input.categoryId],
+ ['brandId', input.brandId],
+ ['sort', input.sort],
+ ],
+ swrOptions: {
+ revalidateOnFocus: false,
+ ...input.swrOptions,
+ },
+ })
+ },
+}
diff --git a/framework/medusa/provider.ts b/framework/medusa/provider.ts
new file mode 100644
index 000000000..e9790eb84
--- /dev/null
+++ b/framework/medusa/provider.ts
@@ -0,0 +1,30 @@
+import { Provider } from '@commerce'
+
+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 { MEDUSA_CART_ID_COOKIE } from './const'
+
+export const medusaProvider: Provider = {
+ locale: 'en-us',
+ cartCookie: MEDUSA_CART_ID_COOKIE,
+ fetcher: fetcher,
+ cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
+ customer: { useCustomer },
+ products: { useSearch },
+ auth: { useLogin, useLogout, useSignup },
+}
+
+export type MedusaProvider = typeof medusaProvider
diff --git a/framework/medusa/types/cart.ts b/framework/medusa/types/cart.ts
new file mode 100644
index 000000000..6ed5c6c64
--- /dev/null
+++ b/framework/medusa/types/cart.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/cart'
diff --git a/framework/medusa/types/checkout.ts b/framework/medusa/types/checkout.ts
new file mode 100644
index 000000000..4e2412ef6
--- /dev/null
+++ b/framework/medusa/types/checkout.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/checkout'
diff --git a/framework/medusa/types/common.ts b/framework/medusa/types/common.ts
new file mode 100644
index 000000000..b52c33a4d
--- /dev/null
+++ b/framework/medusa/types/common.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/common'
diff --git a/framework/medusa/types/customer.ts b/framework/medusa/types/customer.ts
new file mode 100644
index 000000000..87c9afcc4
--- /dev/null
+++ b/framework/medusa/types/customer.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/customer'
diff --git a/framework/medusa/types/index.ts b/framework/medusa/types/index.ts
new file mode 100644
index 000000000..7ab0b7f64
--- /dev/null
+++ b/framework/medusa/types/index.ts
@@ -0,0 +1,25 @@
+import * as Cart from './cart'
+import * as Checkout from './checkout'
+import * as Common from './common'
+import * as Customer from './customer'
+import * as Login from './login'
+import * as Logout from './logout'
+import * as Page from './page'
+import * as Product from './product'
+import * as Signup from './signup'
+import * as Site from './site'
+import * as Wishlist from './wishlist'
+
+export type {
+ Cart,
+ Checkout,
+ Common,
+ Customer,
+ Login,
+ Logout,
+ Page,
+ Product,
+ Signup,
+ Site,
+ Wishlist,
+}
diff --git a/framework/medusa/types/login.ts b/framework/medusa/types/login.ts
new file mode 100644
index 000000000..16bae8f65
--- /dev/null
+++ b/framework/medusa/types/login.ts
@@ -0,0 +1,12 @@
+import * as Core from '@commerce/types/login'
+import type { LoginMutationVariables } from '../schema'
+import { LoginBody, LoginTypes } from '@commerce/types/login'
+
+export * from '@commerce/types/login'
+
+export type LoginHook = {
+ data: null
+ actionInput: LoginBody
+ fetcherInput: LoginBody
+ body: T['body']
+}
diff --git a/framework/medusa/types/logout.ts b/framework/medusa/types/logout.ts
new file mode 100644
index 000000000..9f0a466af
--- /dev/null
+++ b/framework/medusa/types/logout.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/logout'
diff --git a/framework/medusa/types/page.ts b/framework/medusa/types/page.ts
new file mode 100644
index 000000000..20ec8ea38
--- /dev/null
+++ b/framework/medusa/types/page.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/page'
diff --git a/framework/medusa/types/product.ts b/framework/medusa/types/product.ts
new file mode 100644
index 000000000..c776d58fa
--- /dev/null
+++ b/framework/medusa/types/product.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/product'
diff --git a/framework/medusa/types/signup.ts b/framework/medusa/types/signup.ts
new file mode 100644
index 000000000..58543c6f6
--- /dev/null
+++ b/framework/medusa/types/signup.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/signup'
diff --git a/framework/medusa/types/site.ts b/framework/medusa/types/site.ts
new file mode 100644
index 000000000..bfef69cf9
--- /dev/null
+++ b/framework/medusa/types/site.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/site'
diff --git a/framework/medusa/types/wishlist.ts b/framework/medusa/types/wishlist.ts
new file mode 100644
index 000000000..8907fbf82
--- /dev/null
+++ b/framework/medusa/types/wishlist.ts
@@ -0,0 +1 @@
+export * from '@commerce/types/wishlist'
diff --git a/framework/medusa/utils/call-medusa.ts b/framework/medusa/utils/call-medusa.ts
new file mode 100644
index 000000000..745daae9a
--- /dev/null
+++ b/framework/medusa/utils/call-medusa.ts
@@ -0,0 +1,420 @@
+import { CommerceError } from '@commerce/utils/errors'
+import { MEDUSA_PUBLIC_STORE_URL } from '@framework/const'
+import { Product } from '@medusajs/medusa-js/lib/types'
+import Cookies from 'js-cookie'
+import medusa from '../medusa'
+
+export const callMedusa = async (
+ method: string,
+ query: string,
+ variables: any
+) => {
+ switch (query) {
+ case 'auth':
+ if (method === 'authenticate') {
+ const { email, password } = variables
+
+ if (!email || !password) {
+ throw new CommerceError({
+ message: 'An argument for email and password is required',
+ })
+ }
+
+ return await medusa.auth.authenticate({
+ email: email,
+ password: password,
+ })
+ } else if (method === 'exists') {
+ const { email } = variables
+
+ if (!email) {
+ throw new CommerceError({
+ message: 'An argument for email is required',
+ })
+ }
+
+ return await medusa.auth.exists(email)
+ } else if (method === 'getSession') {
+ return await medusa.auth.getSession()
+ } else if ('logout') {
+ //NOT WORKING
+ return await fetch(`${MEDUSA_PUBLIC_STORE_URL}/store/auth`, {
+ method: 'DELETE',
+ })
+ } else {
+ throw new CommerceError({
+ message: 'No valid method argument was provided',
+ })
+ }
+ case 'carts':
+ if (method === 'complete') {
+ const { cart_id } = variables
+
+ if (!cart_id) {
+ throw new CommerceError({
+ message: 'An argument for cart_id is required',
+ })
+ }
+
+ return await medusa.carts.complete(cart_id)
+ } else if (method === 'create') {
+ const { payload } = variables
+
+ return await medusa.carts.create(payload)
+ } else if (method === 'createPaymentSessions') {
+ const { cart_id } = variables
+
+ if (!cart_id) {
+ throw new CommerceError({
+ message: 'An argument for cart_id is required',
+ })
+ }
+
+ return await medusa.carts.createPaymentSessions(cart_id)
+ } else if (method === 'deletePaymentSessions') {
+ const { cart_id, provider_id } = variables
+
+ if (!(cart_id && provider_id)) {
+ throw new CommerceError({
+ message: 'An argument for cart_id and provider_id is required',
+ })
+ }
+
+ return await medusa.carts.deletePaymentSession(cart_id, provider_id)
+ } else if (method === 'refreshPaymentSession') {
+ const { cart_id, provider_id } = variables
+
+ if (!(cart_id && provider_id)) {
+ throw new CommerceError({
+ message: 'An argument for cart_id and provider_id is required',
+ })
+ }
+
+ return await medusa.carts.refreshPaymentSession(cart_id, provider_id)
+ } else if (method === 'updatePaymentSession') {
+ const { cart_id, provider_id, data } = variables
+
+ if (!(cart_id && provider_id)) {
+ throw new CommerceError({
+ message: 'An argument for cart_id and provider_id is required',
+ })
+ }
+
+ return await medusa.carts.updatePaymentSession(cart_id, {
+ provider_id,
+ data,
+ })
+ } else if (method === 'setPaymentSession') {
+ const { cart_id, provider_id } = variables
+
+ if (!(cart_id && provider_id)) {
+ throw new CommerceError({
+ message: 'An argument for cart_id and provider_id is required',
+ })
+ }
+
+ return await medusa.carts.setPaymentSession(cart_id, { provider_id })
+ } else if (method === 'deleteDiscount') {
+ const { cart_id, code } = variables
+
+ if (!(cart_id && code)) {
+ throw new CommerceError({
+ message: 'An argument for cart_id and code is required',
+ })
+ }
+
+ return await medusa.carts.deleteDiscount(cart_id, code)
+ } else if (method === 'retrieve') {
+ const { cart_id } = variables
+
+ if (!cart_id) {
+ throw new CommerceError({
+ message: 'An argument for cart_id and code is required',
+ })
+ }
+
+ return await medusa.carts.retrieve(cart_id)
+ } else if (method === 'update') {
+ const { cart_id, payload } = variables
+
+ if (!(cart_id && payload)) {
+ throw new CommerceError({
+ message: 'An argument for cart_id and payload is required',
+ })
+ }
+ return await medusa.carts.update(cart_id, payload)
+ } else if (method === 'addItem') {
+ const { cart_id, payload } = variables
+ const { variant_id, quantity } = payload
+
+ if (!cart_id) {
+ throw new CommerceError({
+ message: 'An argument for cart_id is required',
+ })
+ }
+ if (!(variant_id && quantity)) {
+ throw new CommerceError({
+ message: 'An argument for variant_id and quantity is required',
+ })
+ }
+
+ return await medusa.carts.lineItems.create(cart_id, {
+ variant_id: variant_id,
+ quantity: quantity,
+ })
+ } else if (method === 'deleteItem') {
+ const { cart_id, line_id } = variables
+
+ if (!(cart_id && line_id)) {
+ throw new CommerceError({
+ message: 'An argument for cart_id and line_id is required',
+ })
+ }
+
+ return await medusa.carts.lineItems.delete(cart_id, line_id)
+ } else if (method === 'updateItem') {
+ const { cart_id, line_id, payload } = variables
+
+ if (!(cart_id && line_id && payload)) {
+ throw new CommerceError({
+ message: 'An argument for cart_id, line_id and payload is required',
+ })
+ }
+ return await medusa.carts.lineItems.update(cart_id, line_id, payload)
+ } else {
+ throw new CommerceError({
+ message: 'No valid method argument was provided',
+ })
+ }
+ case 'customers':
+ if (method === 'addAddresses') {
+ const { customer_id, payload } = variables
+ return await medusa.customers.addresses.addAddress(customer_id, payload)
+ } else if (method === 'updateAddresses') {
+ const { customer_id, address_id, payload } = variables
+ return await medusa.customers.addresses.updateAddress(
+ customer_id,
+ address_id,
+ payload
+ )
+ } else if (method === 'deleteAddress') {
+ const { customer_id, address_id } = variables
+ return await medusa.customers.addresses.deleteAddress(
+ customer_id,
+ address_id
+ )
+ } else if (method === 'listPaymentMethods') {
+ const { customer_id } = variables
+ return await medusa.customers.paymentMethods.list(customer_id)
+ } else if (method === 'create') {
+ const { payload } = variables
+
+ if (!payload) {
+ throw new CommerceError({
+ message: 'An argument for payload is required',
+ })
+ }
+
+ return await medusa.customers.create(payload)
+ } else if (method === 'generatePasswordToken') {
+ const { payload } = variables
+
+ if (!payload) {
+ throw new CommerceError({
+ message: 'An argument for payload is required',
+ })
+ }
+
+ return await medusa.customers.generatePasswordToken(payload)
+ } else if (method === 'listOrders') {
+ const { customer_id } = variables
+
+ if (!customer_id) {
+ throw new CommerceError({
+ message: 'An argument for customer_id is required',
+ })
+ }
+ return await medusa.customers.listOrders(customer_id)
+ } else if (method === 'resetPassword') {
+ const { payload } = variables
+
+ if (!payload) {
+ throw new CommerceError({
+ message: 'An argument for payload is required',
+ })
+ }
+
+ return await medusa.customers.resetPassword(payload)
+ } else if (method === 'retrieve') {
+ const { customer_id } = variables
+
+ if (!customer_id) {
+ throw new CommerceError({
+ message: 'An argument for customer_id is required',
+ })
+ }
+
+ return await medusa.customers.retrieve(customer_id)
+ } else if (method === 'update') {
+ const { customer_id, payload } = variables
+
+ if (!customer_id) {
+ throw new CommerceError({
+ message: 'An argument for customer_id is required',
+ })
+ }
+
+ return await medusa.customers.update(customer_id, payload)
+ }
+ case 'orders':
+ if (method === 'lookupOrder') {
+ const { payload } = variables
+
+ if (!payload) {
+ throw new CommerceError({
+ message: 'An argument for payload is required',
+ })
+ }
+
+ return await medusa.orders.lookupOrder(payload)
+ } else if (method === 'retrieve') {
+ const { order_id } = variables
+
+ if (!order_id) {
+ throw new CommerceError({
+ message: 'An argument for order_id is required',
+ })
+ }
+
+ return await medusa.orders.retrieve(order_id)
+ } else if (method === 'retrieveByCartId') {
+ const { cart_id } = variables
+
+ if (!cart_id) {
+ throw new CommerceError({
+ message: 'An argument for cart_id is required',
+ })
+ }
+
+ return await medusa.orders.retrieveByCartId(cart_id)
+ }
+ case 'products':
+ if (method === 'variantsList') {
+ const { params } = variables
+
+ return await medusa.products.variants.list(params)
+ } else if (method === 'variantsRetrieve') {
+ const { variant_id } = variables
+
+ if (!variant_id) {
+ throw new CommerceError({
+ message: 'An argument for variant_id is required',
+ })
+ }
+
+ return await medusa.products.variants.retrieve(variant_id)
+ } else if (method === 'list') {
+ const { query } = variables
+
+ return await medusa.products.list(
+ query && {
+ limit: query.limit || null,
+ offset: query.offset || null,
+ }
+ )
+ } else if (method === 'retrieve') {
+ const { product_id } = variables
+
+ if (!product_id) {
+ throw new CommerceError({
+ message: 'An argument for product_id is required',
+ })
+ }
+
+ return await medusa.products.retrieve(product_id)
+ } else {
+ throw new CommerceError({
+ message: 'No valid method argument was provided',
+ })
+ }
+ case 'returnReasons':
+ if (method === 'list') {
+ return await medusa.returnReasons.list()
+ } else {
+ throw new CommerceError({
+ message: 'No valid method argument was provided',
+ })
+ }
+ case 'returns':
+ if (method === 'create') {
+ const { payload } = variables
+
+ if (!payload) {
+ throw new CommerceError({
+ message: 'An argument for payload is required',
+ })
+ }
+ return await medusa.returns.create(payload)
+ } else {
+ throw new CommerceError({
+ message: 'No valid method argument was provided',
+ })
+ }
+ case 'shippingOptions':
+ if (method === 'list') {
+ const { cart_id } = variables
+
+ if (!cart_id) {
+ throw new CommerceError({
+ message: 'An argument for cart_id is required',
+ })
+ }
+
+ return await medusa.shippingOptions.list(cart_id)
+ } else if (method === 'create') {
+ const { cart_id } = variables
+
+ if (!cart_id) {
+ throw new CommerceError({
+ message: 'An argument for cart_id is required',
+ })
+ }
+
+ return await medusa.shippingOptions.listCartOptions(cart_id)
+ } else {
+ throw new CommerceError({
+ message: 'No valid method argument was provided',
+ })
+ }
+ case 'swaps':
+ if (method === 'create') {
+ const { cart_id } = variables
+
+ if (!cart_id) {
+ throw new CommerceError({
+ message: 'An argument for cart_id is required',
+ })
+ }
+
+ return await medusa.swaps.create({ cart_id })
+ } else if (method === 'retrieve') {
+ const { cart_id } = variables
+
+ if (!cart_id) {
+ throw new CommerceError({
+ message: 'An argument for cart_id is required',
+ })
+ }
+
+ return await medusa.swaps.retrieveByCartId(cart_id)
+ } else {
+ throw new CommerceError({
+ message: 'No valid method argument was provided',
+ })
+ }
+ default:
+ throw new CommerceError({
+ message: 'No valid query argument was provided',
+ })
+ }
+}
diff --git a/framework/medusa/utils/normalizers/normalize-cart.ts b/framework/medusa/utils/normalizers/normalize-cart.ts
new file mode 100644
index 000000000..48730d9f7
--- /dev/null
+++ b/framework/medusa/utils/normalizers/normalize-cart.ts
@@ -0,0 +1,77 @@
+import { Cart, LineItem, ProductVariant } from '../../types/cart'
+import {
+ Cart as MedusaCart,
+ Discount as MedusaDiscount,
+ LineItem as MedusaLineItem,
+ ProductVariant as MedusaProductVariant,
+} from '@medusajs/medusa-js/lib/types'
+import { Discount } from '@commerce/types/common'
+
+export function normalizeProductVariant(
+ { id, title: name, sku }: MedusaProductVariant,
+ price: number,
+ thumbnail: string
+): ProductVariant {
+ return {
+ id,
+ name,
+ price: price / 100,
+ sku: sku || id,
+ listPrice: price / 100,
+ requiresShipping: true,
+ image: { url: thumbnail, altText: name },
+ }
+}
+
+export function normalizeLineItem({
+ id,
+ title: name,
+ variant,
+ quantity,
+ unit_price,
+ thumbnail,
+}: MedusaLineItem): LineItem {
+ return {
+ id,
+ name,
+ path: variant?.product.handle || name.replace(' ', '-'),
+ variant: normalizeProductVariant(variant!, unit_price, thumbnail!),
+ variantId: variant!.id,
+ productId: variant!.product.id,
+ discounts: [],
+ quantity,
+ }
+}
+
+export function normalizeDiscount(discount: MedusaDiscount): Discount {
+ return {
+ value: discount.rule.value,
+ }
+}
+
+export function normalizeCart({
+ id,
+ email,
+ created_at,
+ region,
+ items,
+ subtotal,
+ total,
+ tax_total,
+ customer_id,
+ discounts,
+}: MedusaCart): Cart {
+ return {
+ id,
+ email,
+ customerId: customer_id,
+ discounts: discounts.map((discount) => normalizeDiscount(discount)),
+ createdAt: `${created_at}`,
+ currency: { code: region.currency_code },
+ lineItems: items.map((item) => normalizeLineItem(item)),
+ subtotalPrice: subtotal / 100,
+ totalPrice: total / 100,
+ taxesIncluded: tax_total > 0,
+ lineItemsSubtotalPrice: subtotal / 100,
+ }
+}
diff --git a/framework/medusa/utils/normalizers/normalize-customer.ts b/framework/medusa/utils/normalizers/normalize-customer.ts
new file mode 100644
index 000000000..7fa3fa7e0
--- /dev/null
+++ b/framework/medusa/utils/normalizers/normalize-customer.ts
@@ -0,0 +1,9 @@
+import { Customer } from '@commerce/types/customer'
+import { Customer as MedusaCustomer } from '@medusajs/medusa-js/lib/types'
+
+export function normalizeCustomer(customer: MedusaCustomer): Customer {
+ return {
+ firstName: customer.first_name,
+ lastName: customer.last_name,
+ }
+}
diff --git a/framework/medusa/utils/normalizers/normalize-products.ts b/framework/medusa/utils/normalizers/normalize-products.ts
new file mode 100644
index 000000000..5a2c8524c
--- /dev/null
+++ b/framework/medusa/utils/normalizers/normalize-products.ts
@@ -0,0 +1,98 @@
+import {
+ Image as MedusaImage,
+ MoneyAmount as MedusaMoneyAmount,
+ Product as MedusaProduct,
+ ProductOption as MedusaProductOption,
+ ProductVariant as MedusaProductVariant,
+} from '@medusajs/medusa-js/lib/types'
+
+import type {
+ Product,
+ ProductImage,
+ ProductOption,
+ ProductPrice,
+ ProductVariant,
+} from '../../types/product'
+
+export function normalizeProductImages(images: MedusaImage[]): ProductImage[] {
+ if (!images || images.length < 1) {
+ return [{ url: '/' }]
+ }
+
+ console.error(images)
+ return images.map(({ url }: MedusaImage) => ({
+ url,
+ }))
+}
+
+export function normalizeAvailability(variant: MedusaProductVariant): boolean {
+ if (
+ variant.manage_inventory &&
+ !variant.allow_backorder &&
+ variant.inventory_quantity < 1
+ )
+ return false
+ return true
+}
+
+export function normalizeProductVariants(
+ variants: MedusaProductVariant[]
+): ProductVariant[] {
+ return variants.map((variant) => {
+ return {
+ id: variant.id,
+ options: [], //variants don't have options
+ availableForSale: normalizeAvailability(variant),
+ }
+ })
+}
+
+export function normalizePrice(price: MedusaMoneyAmount): ProductPrice {
+ return {
+ value: price.amount / 100,
+ currencyCode: price.currency_code.toUpperCase(),
+ }
+}
+
+export function normalizeOptions(
+ options: MedusaProductOption[]
+): ProductOption[] {
+ return options.map((opt) => ({
+ id: opt.id,
+ displayName: opt.title,
+ values: opt.values.map((val) => {
+ return {
+ label: val.value,
+ }
+ }),
+ }))
+}
+
+export function normalizeProduct({
+ id,
+ title: name,
+ description,
+ variants: medusaVariants,
+ options: medusaOptions,
+ images,
+ handle: slug,
+ thumbnail,
+}: MedusaProduct): Product {
+ const tmpVariant = medusaVariants.reduce((prev, curr) =>
+ prev.prices.amount < curr.prices.amount ? prev : curr
+ )
+
+ const minPrice = normalizePrice(tmpVariant.prices[0]) //need to fix typing in medusa types
+
+ return {
+ id,
+ name,
+ description: description || '',
+ variants: normalizeProductVariants(medusaVariants),
+ images: thumbnail && !images.length ? [{ url: thumbnail }] : images,
+ options: normalizeOptions(medusaOptions),
+ price: minPrice,
+ path: `/${slug}`,
+ slug,
+ }
+}
diff --git a/framework/medusa/wishlist/use-add-item.tsx b/framework/medusa/wishlist/use-add-item.tsx
new file mode 100644
index 000000000..75f067c3a
--- /dev/null
+++ b/framework/medusa/wishlist/use-add-item.tsx
@@ -0,0 +1,13 @@
+import { useCallback } from 'react'
+
+export function emptyHook() {
+ const useEmptyHook = async (options = {}) => {
+ return useCallback(async function () {
+ return Promise.resolve()
+ }, [])
+ }
+
+ return useEmptyHook
+}
+
+export default emptyHook
diff --git a/framework/medusa/wishlist/use-remove-item.tsx b/framework/medusa/wishlist/use-remove-item.tsx
new file mode 100644
index 000000000..a2d3a8a05
--- /dev/null
+++ b/framework/medusa/wishlist/use-remove-item.tsx
@@ -0,0 +1,17 @@
+import { useCallback } from 'react'
+
+type Options = {
+ includeProducts?: boolean
+}
+
+export function emptyHook(options?: Options) {
+ const useEmptyHook = async ({ id }: { id: string | number }) => {
+ return useCallback(async function () {
+ return Promise.resolve()
+ }, [])
+ }
+
+ return useEmptyHook
+}
+
+export default emptyHook
diff --git a/framework/medusa/wishlist/use-wishlist.tsx b/framework/medusa/wishlist/use-wishlist.tsx
new file mode 100644
index 000000000..9fe0e758f
--- /dev/null
+++ b/framework/medusa/wishlist/use-wishlist.tsx
@@ -0,0 +1,43 @@
+import { HookFetcher } from '@commerce/utils/types'
+import type { Product } from '@commerce/types/product'
+
+const defaultOpts = {}
+
+export type Wishlist = {
+ items: [
+ {
+ product_id: number
+ variant_id: number
+ id: number
+ product: Product
+ }
+ ]
+}
+
+export interface UseWishlistOptions {
+ includeProducts?: boolean
+}
+
+export interface UseWishlistInput extends UseWishlistOptions {
+ customerId?: number
+}
+
+export const fetcher: HookFetcher = () => {
+ return null
+}
+
+export function extendHook(
+ customFetcher: typeof fetcher,
+ // swrOptions?: SwrOptions
+ swrOptions?: any
+) {
+ const useWishlist = ({ includeProducts }: UseWishlistOptions = {}) => {
+ return { data: null }
+ }
+
+ useWishlist.extend = extendHook
+
+ return useWishlist
+}
+
+export default extendHook(fetcher)
diff --git a/next.config.js b/next.config.js
index 515b2ae7c..dd236c04e 100644
--- a/next.config.js
+++ b/next.config.js
@@ -10,6 +10,7 @@ const isShopify = provider === 'shopify'
const isSaleor = provider === 'saleor'
const isSwell = provider === 'swell'
const isVendure = provider === 'vendure'
+const isMedusa = provider === 'medusa'
module.exports = withCommerceConfig({
commerce,
diff --git a/package.json b/package.json
index 68bf0059d..254bc02d8 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"node": ">=14.x"
},
"dependencies": {
+ "@medusajs/medusa-js": "^1.0.0",
"@react-spring/web": "^9.2.1",
"@vercel/fetch": "^6.1.0",
"autoprefixer": "^10.2.6",
diff --git a/tsconfig.json b/tsconfig.json
index 340929669..fc8466a05 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -23,8 +23,8 @@
"@components/*": ["components/*"],
"@commerce": ["framework/commerce"],
"@commerce/*": ["framework/commerce/*"],
- "@framework": ["framework/local"],
- "@framework/*": ["framework/local/*"]
+ "@framework": ["framework/medusa"],
+ "@framework/*": ["framework/medusa/*"]
}
},
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],
diff --git a/yarn.lock b/yarn.lock
index 35e9ca835..1d0b16199 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -929,6 +929,13 @@
resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.5.tgz#b32366c89b43c6f8cefbdefac778b9c828e3ba8c"
integrity sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==
+"@medusajs/medusa-js@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@medusajs/medusa-js/-/medusa-js-1.0.0.tgz#b8e2231f7d15eebfaf0d5299dc5716ac37171757"
+ integrity sha512-P8l/xI6Q07PHLO65ED1BuS+4BGa2dSLDAkpWGPf3Sc7giKYlgDXo1xXXT8+jnsJklRgWbaJIU9mbcdi79QbFQw==
+ dependencies:
+ axios "^0.21.0"
+
"@microsoft/fetch-event-source@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d"
@@ -1620,6 +1627,13 @@ axe-core@^4.0.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.3.2.tgz#fcf8777b82c62cfc69c7e9f32c0d2226287680e7"
integrity sha512-5LMaDRWm8ZFPAEdzTYmgjjEdj1YnQcpfrVajO/sn/LhbpGp0Y0H64c2hLZI1gRMxfA+w1S71Uc/nHaOXgcCvGg==
+axios@^0.21.0:
+ version "0.21.4"
+ resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
+ integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==
+ dependencies:
+ follow-redirects "^1.14.0"
+
axobject-query@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -3174,6 +3188,11 @@ flatten@^1.0.2:
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
integrity sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==
+follow-redirects@^1.14.0:
+ version "1.14.4"
+ resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379"
+ integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g==
+
foreach@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99"