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 + +

+

+ Medusa Provider +

+

+Medusa is an open-source headless commerce engine that enables developers to create amazing digital commerce experiences. +

+

+ + Medusa is released under the MIT license. + + + PRs welcome! + + + Discord Chat + + + Follow @medusajs + +

+ +## 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"