feat: add medusa-react (#913)

This commit is contained in:
Zakaria El Asri
2021-12-14 19:09:36 +01:00
committed by GitHub
parent 40f6e88875
commit d0d8dd7bf6
97 changed files with 22822 additions and 7595 deletions

View File

@@ -0,0 +1,98 @@
import React, { useState } from "react"
import {
useAddShippingMethodToCart,
useCompleteCart,
useCreateCart,
useSetPaymentSession,
useUpdateCart,
useCreatePaymentSession,
} from "../hooks/carts"
import { Cart } from "../types"
interface CartState {
cart?: Cart
}
interface CartContext extends CartState {
setCart: (cart: Cart) => void
pay: ReturnType<typeof useSetPaymentSession>
createCart: ReturnType<typeof useCreateCart>
startCheckout: ReturnType<typeof useCreatePaymentSession>
completeCheckout: ReturnType<typeof useCompleteCart>
updateCart: ReturnType<typeof useUpdateCart>
addShippingMethod: ReturnType<typeof useAddShippingMethodToCart>
totalItems: number
}
const CartContext = React.createContext<CartContext | null>(null)
export const useCart = () => {
const context = React.useContext(CartContext)
if (!context) {
throw new Error("useCart must be used within a CartProvider")
}
return context
}
interface CartProps {
children: React.ReactNode
initialState?: Cart
}
const defaultInitialState = {
id: "",
items: [] as any,
} as Cart
export const CartProvider = ({
children,
initialState = defaultInitialState,
}: CartProps) => {
const [cart, setCart] = useState<Cart>(initialState)
const createCart = useCreateCart({
onSuccess: ({ cart }) => setCart(cart),
})
const updateCart = useUpdateCart(cart?.id, {
onSuccess: ({ cart }) => setCart(cart),
})
const addShippingMethod = useAddShippingMethodToCart(cart?.id, {
onSuccess: ({ cart }) => setCart(cart),
})
const startCheckout = useCreatePaymentSession(cart?.id, {
onSuccess: ({ cart }) => setCart(cart),
})
const pay = useSetPaymentSession(cart?.id, {
onSuccess: ({ cart }) => {
setCart(cart)
},
})
const completeCheckout = useCompleteCart(cart?.id)
const totalItems = cart?.items
.map((i) => i.quantity)
.reduce((acc, curr) => acc + curr, 0)
return (
<CartContext.Provider
value={{
cart,
setCart,
createCart,
pay,
startCheckout,
completeCheckout,
updateCart,
addShippingMethod,
totalItems: totalItems || 0,
}}
>
{children}
</CartContext.Provider>
)
}

View File

@@ -0,0 +1,3 @@
export * from "./medusa"
export * from "./session-cart"
export * from "./cart"

View File

@@ -0,0 +1,38 @@
import React from "react"
import { QueryClientProvider, QueryClientProviderProps } from "react-query"
import Medusa from "@medusajs/medusa-js"
interface MedusaContextState {
client: Medusa
}
const MedusaContext = React.createContext<MedusaContextState | null>(null)
export const useMedusa = () => {
const context = React.useContext(MedusaContext)
if (!context) {
throw new Error("useMedusa must be used within a MedusaProvider")
}
return context
}
interface MedusaProviderProps {
baseUrl: string
queryClientProviderProps: QueryClientProviderProps
children: React.ReactNode
}
export const MedusaProvider = ({
queryClientProviderProps,
baseUrl,
children,
}: MedusaProviderProps) => {
const medusaClient = new Medusa({ baseUrl, maxRetries: 0 })
return (
<QueryClientProvider {...queryClientProviderProps}>
<MedusaContext.Provider value={{ client: medusaClient }}>
{children}
</MedusaContext.Provider>
</QueryClientProvider>
)
}

View File

@@ -0,0 +1,289 @@
import React, { useContext, useEffect } from "react"
import { useLocalStorage } from "../hooks/utils"
import { RegionInfo, ProductVariant } from "../types"
import { getVariantPrice } from "../utils"
import { isArray, isEmpty, isObject } from "lodash"
interface Item {
variant: ProductVariant
quantity: number
readonly total?: number
}
export interface SessionCartState {
region: RegionInfo
items: Item[]
totalItems: number
total: number
}
interface SessionCartContextState extends SessionCartState {
setRegion: (region: RegionInfo) => void
addItem: (item: Item) => void
removeItem: (id: string) => void
updateItem: (id: string, item: Partial<Item>) => void
setItems: (items: Item[]) => void
updateItemQuantity: (id: string, quantity: number) => void
incrementItemQuantity: (id: string) => void
decrementItemQuantity: (id: string) => void
getItem: (id: string) => Item | undefined
clearItems: () => void
}
const SessionCartContext = React.createContext<SessionCartContextState | null>(
null
)
enum ACTION_TYPES {
INIT,
ADD_ITEM,
SET_ITEMS,
REMOVE_ITEM,
UPDATE_ITEM,
CLEAR_ITEMS,
SET_REGION,
}
type Action =
| { type: ACTION_TYPES.SET_REGION; payload: RegionInfo }
| { type: ACTION_TYPES.INIT; payload: object }
| { type: ACTION_TYPES.ADD_ITEM; payload: Item }
| {
type: ACTION_TYPES.UPDATE_ITEM
payload: { id: string; item: Partial<Item> }
}
| { type: ACTION_TYPES.REMOVE_ITEM; payload: { id: string } }
| { type: ACTION_TYPES.SET_ITEMS; payload: Item[] }
| { type: ACTION_TYPES.CLEAR_ITEMS }
const reducer = (state: SessionCartState, action: Action) => {
switch (action.type) {
case ACTION_TYPES.INIT: {
return state
}
case ACTION_TYPES.SET_REGION: {
return generateCartState(
{
...state,
region: action.payload,
},
state.items
)
}
case ACTION_TYPES.ADD_ITEM: {
const duplicateVariantIndex = state.items.findIndex(
(item) => item.variant.id === action.payload?.variant?.id
)
if (duplicateVariantIndex !== -1) {
state.items.splice(duplicateVariantIndex, 1)
}
const items = [...state.items, action.payload]
return generateCartState(state, items)
}
case ACTION_TYPES.UPDATE_ITEM: {
const items = state.items.map((item) =>
item.variant.id === action.payload.id
? { ...item, ...action.payload.item }
: item
)
return generateCartState(state, items)
}
case ACTION_TYPES.REMOVE_ITEM: {
const items = state.items.filter(
(item) => item.variant.id !== action.payload.id
)
return generateCartState(state, items)
}
case ACTION_TYPES.SET_ITEMS: {
return generateCartState(state, action.payload)
}
case ACTION_TYPES.CLEAR_ITEMS: {
return {
...state,
items: [],
total: 0,
totalItems: 0,
}
}
default:
return state
}
}
export const generateCartState = (state: SessionCartState, items: Item[]) => {
const newItems = generateItems(state.region, items)
return {
...state,
items: newItems,
totalItems: items.reduce((sum, item) => sum + item.quantity, 0),
total: calculateSessionCartTotal(newItems),
}
}
const generateItems = (region: RegionInfo, items: Item[]) => {
return items.map((item) => ({
...item,
total: getVariantPrice(item.variant, region),
}))
}
const calculateSessionCartTotal = (items: Item[]) => {
return items.reduce(
(total, item) => total + item.quantity * (item.total || 0),
0
)
}
interface SessionCartProviderProps {
children: React.ReactNode
initialState?: SessionCartState
}
const defaultInitialState: SessionCartState = {
region: {} as RegionInfo,
items: [],
total: 0,
totalItems: 0,
}
export const SessionCartProvider = ({
initialState = defaultInitialState,
children,
}: SessionCartProviderProps) => {
const [saved, save] = useLocalStorage(
"medusa-session-cart",
JSON.stringify(initialState)
)
const [state, dispatch] = React.useReducer(reducer, JSON.parse(saved))
useEffect(() => {
save(JSON.stringify(state))
}, [state, save])
const setRegion = (region: RegionInfo) => {
if (!isObject(region) || isEmpty(region)) {
throw new Error("region must be a non-empty object")
}
dispatch({ type: ACTION_TYPES.SET_REGION, payload: region })
}
const getItem = (id: string) => {
return state.items.find((item) => item.variant.id === id)
}
const setItems = (items: Item[]) => {
if (!isArray(items)) {
throw new Error("items must be an array of items")
}
dispatch({ type: ACTION_TYPES.SET_ITEMS, payload: items })
}
const addItem = (item: Item) => {
if (!isObject(item) || isEmpty(item)) {
throw new Error("item must be a non-empty object")
}
dispatch({ type: ACTION_TYPES.ADD_ITEM, payload: item })
}
const updateItem = (id: string, item: Partial<Item>) => {
dispatch({ type: ACTION_TYPES.UPDATE_ITEM, payload: { id, item } })
}
const updateItemQuantity = (id: string, quantity: number) => {
const item = getItem(id)
if (!item) return
quantity = quantity <= 0 ? 1 : quantity
dispatch({
type: ACTION_TYPES.UPDATE_ITEM,
payload: {
id,
item: {
...item,
quantity: Math.min(item.variant.inventory_quantity, quantity),
},
},
})
}
const incrementItemQuantity = (id: string) => {
const item = getItem(id)
if (!item) return
dispatch({
type: ACTION_TYPES.UPDATE_ITEM,
payload: {
id,
item: {
...item,
quantity: Math.min(
item.variant.inventory_quantity,
item.quantity + 1
),
},
},
})
}
const decrementItemQuantity = (id: string) => {
const item = getItem(id)
if (!item) return
dispatch({
type: ACTION_TYPES.UPDATE_ITEM,
payload: {
id,
item: { ...item, quantity: Math.max(0, item.quantity - 1) },
},
})
}
const removeItem = (id: string) => {
dispatch({
type: ACTION_TYPES.REMOVE_ITEM,
payload: { id },
})
}
const clearItems = () => {
dispatch({
type: ACTION_TYPES.CLEAR_ITEMS,
})
}
return (
<SessionCartContext.Provider
value={{
...state,
setRegion,
addItem,
updateItem,
updateItemQuantity,
incrementItemQuantity,
decrementItemQuantity,
removeItem,
getItem,
setItems,
clearItems,
}}
>
{children}
</SessionCartContext.Provider>
)
}
export const useSessionCart = () => {
const context = useContext(SessionCartContext)
if (!context) {
throw new Error(
"useSessionCart should be used as a child of SessionCartProvider"
)
}
return context
}

View File

@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"

View File

@@ -0,0 +1,156 @@
import {
StoreCartsRes,
StoreCompleteCartRes,
StorePostCartReq,
StorePostCartsCartPaymentSessionReq,
StorePostCartsCartPaymentSessionUpdateReq,
StorePostCartsCartReq,
StorePostCartsCartShippingMethodReq,
} from "@medusajs/medusa"
import { useMutation, UseMutationOptions } from "react-query"
import { useMedusa } from "../../contexts/medusa"
export const useCreateCart = (
options?: UseMutationOptions<
StoreCartsRes,
Error,
StorePostCartReq | undefined
>
) => {
const { client } = useMedusa()
return useMutation(
(data?: StorePostCartReq | undefined) => client.carts.create(data),
options
)
}
export const useUpdateCart = (
cartId: string,
options?: UseMutationOptions<StoreCartsRes, Error, StorePostCartsCartReq>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostCartsCartReq) => client.carts.update(cartId, data),
options
)
}
export const useCompleteCart = (
cartId: string,
options?: UseMutationOptions<StoreCompleteCartRes, Error>
) => {
const { client } = useMedusa()
return useMutation(() => client.carts.complete(cartId), options)
}
export const useCreatePaymentSession = (
cartId: string,
options?: UseMutationOptions<StoreCartsRes, Error>
) => {
const { client } = useMedusa()
return useMutation(() => client.carts.createPaymentSessions(cartId), options)
}
export const useUpdatePaymentSession = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
{ provider_id: string } & StorePostCartsCartPaymentSessionUpdateReq
>
) => {
const { client } = useMedusa()
return useMutation(
(
data: { provider_id: string } & StorePostCartsCartPaymentSessionUpdateReq
) => client.carts.updatePaymentSession(cartId, data.provider_id, data),
options
)
}
type RefreshPaymentSessionMutationData = {
provider_id: string
}
export const useRefreshPaymentSession = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
RefreshPaymentSessionMutationData
>
) => {
const { client } = useMedusa()
return useMutation(
({ provider_id }: RefreshPaymentSessionMutationData) =>
client.carts.refreshPaymentSession(cartId, provider_id),
options
)
}
type SetPaymentSessionMutationData = { provider_id: string }
export const useSetPaymentSession = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
SetPaymentSessionMutationData
>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostCartsCartPaymentSessionReq) =>
client.carts.setPaymentSession(cartId, data),
options
)
}
export const useAddShippingMethodToCart = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
StorePostCartsCartShippingMethodReq
>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostCartsCartShippingMethodReq) =>
client.carts.addShippingMethod(cartId, data),
options
)
}
type DeletePaymentSessionMutationData = {
provider_id: string
}
export const useDeletePaymentSession = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
DeletePaymentSessionMutationData
>
) => {
const { client } = useMedusa()
return useMutation(
({ provider_id }: DeletePaymentSessionMutationData) =>
client.carts.deletePaymentSession(cartId, provider_id),
options
)
}
export const useStartCheckout = (
options?: UseMutationOptions<StoreCartsRes["cart"], Error, StorePostCartReq>
) => {
const { client } = useMedusa()
const mutation = useMutation(async (data?: StorePostCartReq) => {
const { cart } = await client.carts.create(data)
const res = await client.carts.createPaymentSessions(cart.id)
return res.cart
}, options)
return mutation
}

View File

@@ -0,0 +1,28 @@
import { makeKeysFactory } from "./../utils/index"
import { StoreCartsRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts/medusa"
import { UseQueryOptionsWrapper } from "../../types"
const CARTS_QUERY_KEY = `carts` as const
export const cartKeys = makeKeysFactory(CARTS_QUERY_KEY)
type CartQueryKey = typeof cartKeys
export const useGetCart = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreCartsRes>,
Error,
ReturnType<CartQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
cartKeys.detail(id),
() => client.carts.retrieve(id),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1 @@
export * from "./queries"

View File

@@ -0,0 +1,50 @@
import {
StoreCollectionsListRes,
StoreCollectionsRes,
StoreGetCollectionsParams,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts/medusa"
import { UseQueryOptionsWrapper } from "./../../types"
import { makeKeysFactory } from "./../utils/index"
const COLLECTIONS_QUERY_KEY = `collections` as const
export const collectionKeys = makeKeysFactory(COLLECTIONS_QUERY_KEY)
type CollectionQueryKey = typeof collectionKeys
export const useCollection = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreCollectionsRes>,
Error,
ReturnType<CollectionQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
collectionKeys.detail(id),
() => client.collections.retrieve(id),
options
)
return { ...data, ...rest } as const
}
export const useCollections = (
query?: StoreGetCollectionsParams,
options?: UseQueryOptionsWrapper<
Response<StoreCollectionsListRes>,
Error,
ReturnType<CollectionQueryKey["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
collectionKeys.list(query),
() => client.collections.list(query),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"

View File

@@ -0,0 +1,32 @@
import {
StoreCustomersRes,
StorePostCustomersCustomerReq,
StorePostCustomersReq,
} from "@medusajs/medusa"
import { useMutation, UseMutationOptions } from "react-query"
import { useMedusa } from "../../contexts/medusa"
export const useCreateCustomer = (
options?: UseMutationOptions<StoreCustomersRes, Error, StorePostCustomersReq>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostCustomersReq) => client.customers.create(data),
options
)
}
export const useUpdateMe = (
options?: UseMutationOptions<
StoreCustomersRes,
Error,
{ id: string } & StorePostCustomersCustomerReq
>
) => {
const { client } = useMedusa()
return useMutation(
({ id, ...data }: { id: string } & StorePostCustomersCustomerReq) =>
client.customers.update(data),
options
)
}

View File

@@ -0,0 +1,53 @@
import {
StoreCustomersListOrdersRes,
StoreCustomersRes,
StoreGetCustomersCustomerOrdersParams,
} from "@medusajs/medusa"
import { useQuery } from "react-query"
import { Response } from "@medusajs/medusa-js"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
import { makeKeysFactory } from "./../utils/index"
const CUSTOMERS_QUERY_KEY = `customers` as const
export const customerKeys = {
...makeKeysFactory(CUSTOMERS_QUERY_KEY),
orders: (id: string) => [...customerKeys.detail(id), "orders"] as const,
}
type CustomerQueryKey = typeof customerKeys
export const useMeCustomer = (
options?: UseQueryOptionsWrapper<
Response<StoreCustomersRes>,
Error,
ReturnType<CustomerQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
customerKeys.detail("me"),
() => client.customers.retrieve(),
options
)
return { ...data, ...rest } as const
}
export const useCustomerOrders = (
query: StoreGetCustomersCustomerOrdersParams = { limit: 10, offset: 0 },
options?: UseQueryOptionsWrapper<
Response<StoreCustomersListOrdersRes>,
Error,
ReturnType<CustomerQueryKey["orders"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
customerKeys.orders("me"),
() => client.customers.listOrders(query),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1 @@
export * from "./queries"

View File

@@ -0,0 +1,29 @@
import { makeKeysFactory } from "./../utils/index"
import { StoreGiftCardsRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
const GIFT_CARDS_QUERY_KEY = `gift_cards` as const
export const giftCardKeys = makeKeysFactory(GIFT_CARDS_QUERY_KEY)
type GiftCardQueryKey = typeof giftCardKeys
export const useGiftCard = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreGiftCardsRes>,
Error,
ReturnType<GiftCardQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
giftCardKeys.detail(id),
() => client.giftCards.retrieve(id),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1,13 @@
export * from "./products/"
export * from "./carts/"
export * from "./shipping-options/"
export * from "./regions/"
export * from "./return-reasons/"
export * from "./swaps/"
export * from "./carts/"
export * from "./orders/"
export * from "./customers/"
export * from "./returns/"
export * from "./gift-cards/"
export * from "./line-items/"
export * from "./collections"

View File

@@ -0,0 +1 @@
export * from "./mutations"

View File

@@ -0,0 +1,54 @@
import {
StoreCartsRes,
StorePostCartsCartLineItemsReq,
StorePostCartsCartLineItemsItemReq,
} from "@medusajs/medusa"
import { useMutation, UseMutationOptions } from "react-query"
import { useMedusa } from "../../contexts"
export const useCreateLineItem = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
StorePostCartsCartLineItemsReq
>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostCartsCartLineItemsReq) =>
client.carts.lineItems.create(cartId, data),
options
)
}
export const useUpdateLineItem = (
cartId: string,
options?: UseMutationOptions<
StoreCartsRes,
Error,
StorePostCartsCartLineItemsItemReq & { lineId: string }
>
) => {
const { client } = useMedusa()
return useMutation(
({
lineId,
...data
}: StorePostCartsCartLineItemsItemReq & { lineId: string }) =>
client.carts.lineItems.update(cartId, lineId, data),
options
)
}
export const useDeleteLineItem = (
cartId: string,
options?: UseMutationOptions<StoreCartsRes, Error, { lineId: string }>
) => {
const { client } = useMedusa()
return useMutation(
({ lineId }: { lineId: string }) =>
client.carts.lineItems.delete(cartId, lineId),
options
)
}

View File

@@ -0,0 +1 @@
export * from "./queries"

View File

@@ -0,0 +1,71 @@
import { makeKeysFactory } from "./../utils/index"
import { StoreOrdersRes, StoreGetOrdersParams } from "@medusajs/medusa"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
import { Response } from "@medusajs/medusa-js"
const ORDERS_QUERY_KEY = `orders` as const
export const orderKeys = {
...makeKeysFactory<typeof ORDERS_QUERY_KEY, StoreGetOrdersParams>(
ORDERS_QUERY_KEY
),
cart: (cartId: string) => [...orderKeys.details(), "cart", cartId] as const,
}
type OrderQueryKey = typeof orderKeys
export const useOrder = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreOrdersRes>,
Error,
ReturnType<OrderQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
orderKeys.detail(id),
() => client.orders.retrieve(id),
options
)
return { ...data, ...rest } as const
}
export const useCartOrder = (
cartId: string,
options?: UseQueryOptionsWrapper<
Response<StoreOrdersRes>,
Error,
ReturnType<OrderQueryKey["cart"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
orderKeys.cart(cartId),
() => client.orders.retrieveByCartId(cartId),
options
)
return { ...data, ...rest } as const
}
export const useOrderLookup = (
query: StoreGetOrdersParams,
options?: UseQueryOptionsWrapper<
Response<StoreOrdersRes>,
Error,
ReturnType<OrderQueryKey["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
orderKeys.list(query),
() => client.orders.lookupOrder(query),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1 @@
export * from "./queries"

View File

@@ -0,0 +1,53 @@
import { Response } from "@medusajs/medusa-js"
import {
StoreGetProductsParams,
StoreProductsListRes,
StoreProductsRes,
} from "@medusajs/medusa"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
import { makeKeysFactory } from "./../utils/index"
const PRODUCTS_QUERY_KEY = `products` as const
export const productKeys = makeKeysFactory<
typeof PRODUCTS_QUERY_KEY,
StoreGetProductsParams
>(PRODUCTS_QUERY_KEY)
type ProductQueryKey = typeof productKeys
export const useProducts = (
query?: StoreGetProductsParams,
options?: UseQueryOptionsWrapper<
Response<StoreProductsListRes>,
Error,
ReturnType<ProductQueryKey["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
productKeys.list(query),
() => client.products.list(query),
options
)
return { ...data, ...rest } as const
}
export const useProduct = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreProductsRes>,
Error,
ReturnType<ProductQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
productKeys.detail(id),
() => client.products.retrieve(id),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1 @@
export * from "./queries"

View File

@@ -0,0 +1,45 @@
import { makeKeysFactory } from "./../utils/index"
import { UseQueryOptionsWrapper } from "../../types"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { StoreRegionsRes, StoreRegionsListRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
const REGIONS_QUERY_KEY = `regions` as const
const regionsKey = makeKeysFactory(REGIONS_QUERY_KEY)
type RegionQueryType = typeof regionsKey
export const useRegions = (
options?: UseQueryOptionsWrapper<
Response<StoreRegionsListRes>,
Error,
ReturnType<RegionQueryType["lists"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
regionsKey.lists(),
() => client.regions.list(),
options
)
return { ...data, ...rest } as const
}
export const useRegion = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreRegionsRes>,
Error,
ReturnType<RegionQueryType["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
regionsKey.detail(id),
() => client.regions.retrieve(id),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1 @@
export * from "./queries"

View File

@@ -0,0 +1,48 @@
import { makeKeysFactory } from "./../utils/index"
import {
StoreReturnReasonsListRes,
StoreReturnReasonsRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
const RETURNS_REASONS_QUERY_KEY = `return_reasons` as const
const returnReasonsKey = makeKeysFactory(RETURNS_REASONS_QUERY_KEY)
type ReturnReasonsQueryKey = typeof returnReasonsKey
export const useReturnReasons = (
options?: UseQueryOptionsWrapper<
Response<StoreReturnReasonsListRes>,
Error,
ReturnType<ReturnReasonsQueryKey["lists"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
returnReasonsKey.lists(),
() => client.returnReasons.list(),
options
)
return { ...data, ...rest } as const
}
export const useReturnReason = (
id: string,
options?: UseQueryOptionsWrapper<
Response<StoreReturnReasonsRes>,
Error,
ReturnType<ReturnReasonsQueryKey["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
returnReasonsKey.detail(id),
() => client.returnReasons.retrieve(id),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1 @@
export * from "./mutations"

View File

@@ -0,0 +1,13 @@
import { StoreReturnsRes, StorePostReturnsReq } from "@medusajs/medusa"
import { useMutation, UseMutationOptions } from "react-query"
import { useMedusa } from "../../contexts"
export const useCreateReturn = (
options?: UseMutationOptions<StoreReturnsRes, Error, StorePostReturnsReq>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostReturnsReq) => client.returns.create(data),
options
)
}

View File

@@ -0,0 +1 @@
export * from "./queries"

View File

@@ -0,0 +1,52 @@
import { makeKeysFactory } from "./../utils/index"
import { UseQueryOptionsWrapper } from "../../types"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import {
StoreShippingOptionsListRes,
StoreGetShippingOptionsParams,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
const SHIPPING_OPTION_QUERY_KEY = `shipping_options` as const
const shippingOptionKey = {
...makeKeysFactory(SHIPPING_OPTION_QUERY_KEY),
cart: (cartId: string) => [...shippingOptionKey.all, "cart", cartId] as const,
}
type ShippingOptionQueryKey = typeof shippingOptionKey
export const useShippingOptions = (
query?: StoreGetShippingOptionsParams,
options?: UseQueryOptionsWrapper<
Response<StoreShippingOptionsListRes>,
Error,
ReturnType<ShippingOptionQueryKey["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
shippingOptionKey.list(query),
async () => client.shippingOptions.list(query),
options
)
return { ...data, ...rest } as const
}
export const useCartShippingOptions = (
cartId: string,
options?: UseQueryOptionsWrapper<
Response<StoreShippingOptionsListRes>,
Error,
ReturnType<ShippingOptionQueryKey["cart"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
shippingOptionKey.cart(cartId),
async () => client.shippingOptions.listCartOptions(cartId),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"

View File

@@ -0,0 +1,13 @@
import { StoreSwapsRes, StorePostSwapsReq } from "@medusajs/medusa"
import { useMutation, UseMutationOptions } from "react-query"
import { useMedusa } from "../../contexts"
export const useCreateSwap = (
options?: UseMutationOptions<StoreSwapsRes, Error, StorePostSwapsReq>
) => {
const { client } = useMedusa()
return useMutation(
(data: StorePostSwapsReq) => client.swaps.create(data),
options
)
}

View File

@@ -0,0 +1,33 @@
import { makeKeysFactory } from "./../utils/index"
import { StoreSwapsRes } from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../contexts"
import { UseQueryOptionsWrapper } from "../../types"
const SWAPS_QUERY_KEY = `swaps` as const
const swapKey = {
...makeKeysFactory(SWAPS_QUERY_KEY),
cart: (cartId: string) => [...swapKey.all, "cart", cartId] as const,
}
type SwapQueryKey = typeof swapKey
export const useCartSwap = (
cartId: string,
options?: UseQueryOptionsWrapper<
Response<StoreSwapsRes>,
Error,
ReturnType<SwapQueryKey["cart"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
swapKey.cart(cartId),
() => client.swaps.retrieveByCartId(cartId),
options
)
return { ...data, ...rest } as const
}

View File

@@ -0,0 +1,62 @@
import * as React from "react"
type TQueryKey<TKey, TListQuery = any, TDetailQuery = string> = {
all: [TKey]
lists: () => [...TQueryKey<TKey>["all"], "list"]
list: (
query?: TListQuery
) => [
...ReturnType<TQueryKey<TKey>["lists"]>,
{ query: TListQuery | undefined }
]
details: () => [...TQueryKey<TKey>["all"], "detail"]
detail: (
id: TDetailQuery
) => [...ReturnType<TQueryKey<TKey>["details"]>, TDetailQuery]
}
export const makeKeysFactory = <
T,
TListQueryType = any,
TDetailQueryType = string
>(
globalKey: T
) => {
const queryKeyFactory: TQueryKey<T, TListQueryType, TDetailQueryType> = {
all: [globalKey],
lists: () => [...queryKeyFactory.all, "list"],
list: (query?: TListQueryType) => [...queryKeyFactory.lists(), { query }],
details: () => [...queryKeyFactory.all, "detail"],
detail: (id: TDetailQueryType) => [...queryKeyFactory.details(), id],
}
return queryKeyFactory
}
export const useLocalStorage = (key: string, initialState: string) => {
const [item, setItem] = React.useState(() => {
try {
const item =
typeof window !== "undefined" && window.localStorage.getItem(key)
return item || initialState
} catch (err) {
return initialState
}
})
const save = (data: string) => {
setItem(data)
if (typeof window !== "undefined") {
window.localStorage.setItem(key, data)
}
}
const remove = () => {
if (typeof window !== "undefined") {
window.localStorage.removeItem(key)
}
}
return [item, save, remove] as const
}

View File

@@ -0,0 +1,3 @@
export * from "./contexts"
export * from "./hooks/"
export * from "./utils/"

View File

@@ -0,0 +1,31 @@
import {
Region,
ProductVariant as ProductVariantEntity,
StoreCartsRes,
} from "@medusajs/medusa"
import { QueryKey, UseQueryOptions } from "react-query"
export type UseQueryOptionsWrapper<
// Return type of queryFn
TQueryFn = unknown,
// Type thrown in case the queryFn rejects
E = Error,
// Query key type
TQueryKey extends QueryKey = QueryKey
> = Omit<
UseQueryOptions<TQueryFn, E, TQueryFn, TQueryKey>,
"queryKey" | "queryFn" | "select" | "refetchInterval"
>
// Choose only a subset of the type Region to allow for some flexibility
export type RegionInfo = Pick<Region, "currency_code" | "tax_code" | "tax_rate">
export type ProductVariant = ConvertDateToString<
Omit<ProductVariantEntity, "beforeInsert">
>
export type ProductVariantInfo = Pick<ProductVariant, "prices">
type ConvertDateToString<T extends {}> = {
[P in keyof T]: T[P] extends Date ? Date | string : T[P]
}
export type Cart = StoreCartsRes["cart"]

View File

@@ -0,0 +1,168 @@
import { isEmpty } from "lodash"
import { RegionInfo, ProductVariantInfo } from "../types"
type FormatVariantPriceParams = {
variant: ProductVariantInfo
region: RegionInfo
includeTaxes?: boolean
minimumFractionDigits?: number
maximumFractionDigits?: number
locale?: string
}
/**
* Takes a product variant and a region, and converts the variant's price to a localized decimal format
*/
export const formatVariantPrice = ({
variant,
region,
includeTaxes = true,
...rest
}: FormatVariantPriceParams) => {
const amount = computeVariantPrice({ variant, region, includeTaxes })
return convertToLocale({
amount,
currency_code: region?.currency_code,
...rest,
})
}
type ComputeVariantPriceParams = {
variant: ProductVariantInfo
region: RegionInfo
includeTaxes?: boolean
}
/**
* Takes a product variant and region, and returns the variant price as a decimal number
* @param params.variant - product variant
* @param params.region - region
* @param params.includeTaxes - whether to include taxes or not
*/
export const computeVariantPrice = ({
variant,
region,
includeTaxes = true,
}: ComputeVariantPriceParams) => {
const amount = getVariantPrice(variant, region)
return computeAmount({
amount,
region,
includeTaxes,
})
}
/**
* Finds the price amount correspoding to the region selected
* @param variant - the product variant
* @param region - the region
* @returns - the price's amount
*/
export const getVariantPrice = (
variant: ProductVariantInfo,
region: RegionInfo
) => {
let price = variant?.prices?.find(
(p) =>
p.currency_code.toLowerCase() === region?.currency_code?.toLowerCase()
)
return price?.amount || 0
}
type ComputeAmountParams = {
amount: number
region: RegionInfo
includeTaxes?: boolean
}
/**
* Takes an amount, a region, and returns the amount as a decimal including or excluding taxes
*/
export const computeAmount = ({
amount,
region,
includeTaxes = true,
}: ComputeAmountParams) => {
const toDecimal = convertToDecimal(amount, region)
const taxRate = includeTaxes ? getTaxRate(region) : 0
const amountWithTaxes = toDecimal * (1 + taxRate)
return amountWithTaxes
}
type FormatAmountParams = {
amount: number
region: RegionInfo
includeTaxes?: boolean
minimumFractionDigits?: number
maximumFractionDigits?: number
locale?: string
}
/**
* Takes an amount and a region, and converts the amount to a localized decimal format
*/
export const formatAmount = ({
amount,
region,
includeTaxes = true,
...rest
}: FormatAmountParams) => {
const taxAwareAmount = computeAmount({
amount,
region,
includeTaxes,
})
return convertToLocale({
amount: taxAwareAmount,
currency_code: region.currency_code,
...rest,
})
}
// we should probably add a more extensive list
const noDivisionCurrencies = ["krw", "jpy", "vnd"]
const convertToDecimal = (amount: number, region: RegionInfo) => {
const divisor = noDivisionCurrencies.includes(
region?.currency_code?.toLowerCase()
)
? 1
: 100
return Math.floor(amount) / divisor
}
const getTaxRate = (region?: RegionInfo) => {
return region && !isEmpty(region) ? region?.tax_rate / 100 : 0
}
const convertToLocale = ({
amount,
currency_code,
minimumFractionDigits,
maximumFractionDigits,
locale = "en-US",
}: ConvertToLocaleParams) => {
return currency_code && !isEmpty(currency_code)
? new Intl.NumberFormat(locale, {
style: "currency",
currency: currency_code,
minimumFractionDigits,
maximumFractionDigits,
}).format(amount)
: amount.toString()
}
type ConvertToLocaleParams = {
amount: number
currency_code: string
minimumFractionDigits?: number
maximumFractionDigits?: number
locale?: string
}