mirror of
https://github.com/vercel/commerce.git
synced 2025-07-26 19:51:23 +00:00
Implement Shopify Provider
This commit is contained in:
3
framework/shopify/cart/index.ts
Normal file
3
framework/shopify/cart/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as useCart } from './use-cart'
|
||||
export { default as useAddItem } from './use-add-item'
|
||||
export { default as useRemoveItem } from './use-remove-item'
|
70
framework/shopify/cart/use-add-item.tsx
Normal file
70
framework/shopify/cart/use-add-item.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useCallback } from 'react'
|
||||
import { CommerceError } from '@commerce/utils/errors'
|
||||
import useCart from './use-cart'
|
||||
import useCartAddItem, {
|
||||
AddItemInput as UseAddItemInput,
|
||||
} from '@commerce/cart/use-add-item'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { Cart } from '@commerce/types'
|
||||
import checkoutLineItemAddMutation from '../utils/mutations/checkout-line-item-add'
|
||||
import getCheckoutId from '@framework/utils/get-checkout-id'
|
||||
import { checkoutToCart } from './utils'
|
||||
|
||||
const defaultOpts = {
|
||||
query: checkoutLineItemAddMutation,
|
||||
}
|
||||
|
||||
export type AddItemInput = UseAddItemInput<any>
|
||||
|
||||
export const fetcher: HookFetcher<Cart, any> = async (
|
||||
options,
|
||||
{ checkoutId, item },
|
||||
fetch
|
||||
) => {
|
||||
if (
|
||||
item.quantity &&
|
||||
(!Number.isInteger(item.quantity) || item.quantity! < 1)
|
||||
) {
|
||||
throw new CommerceError({
|
||||
message: 'The item quantity has to be a valid integer greater than 0',
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fetch<any, any>({
|
||||
...options,
|
||||
variables: {
|
||||
checkoutId,
|
||||
lineItems: [item],
|
||||
},
|
||||
})
|
||||
|
||||
return checkoutToCart(data?.checkoutLineItemsAdd)
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useAddItem = () => {
|
||||
const { mutate, data: cart } = useCart()
|
||||
const fn = useCartAddItem(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
async function addItem(input: AddItemInput) {
|
||||
const data = await fn({
|
||||
item: {
|
||||
variantId: input.variantId,
|
||||
quantity: input.quantity ?? 1,
|
||||
},
|
||||
checkoutId: getCheckoutId(cart?.id),
|
||||
})
|
||||
await mutate(data, false)
|
||||
return data
|
||||
},
|
||||
[fn, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useAddItem.extend = extendHook
|
||||
|
||||
return useAddItem
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
67
framework/shopify/cart/use-cart.tsx
Normal file
67
framework/shopify/cart/use-cart.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import type { SwrOptions } from '@commerce/utils/use-data'
|
||||
|
||||
import useResponse from '@commerce/utils/use-response'
|
||||
import useCommerceCart, { CartInput } from '@commerce/cart/use-cart'
|
||||
import getCheckoutQuery from '@framework/utils/queries/get-checkout-query'
|
||||
|
||||
import { Cart } from '@commerce/types'
|
||||
import { checkoutToCart, checkoutCreate } from './utils'
|
||||
|
||||
const defaultOpts = {
|
||||
query: getCheckoutQuery,
|
||||
}
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, CartInput> = async (
|
||||
options,
|
||||
{ cartId: checkoutId },
|
||||
fetch
|
||||
) => {
|
||||
let checkout
|
||||
|
||||
if (checkoutId) {
|
||||
const data = await fetch({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
variables: {
|
||||
checkoutId,
|
||||
},
|
||||
})
|
||||
checkout = data?.node
|
||||
}
|
||||
|
||||
if (checkout?.completedAt || !checkoutId) {
|
||||
checkout = await checkoutCreate(fetch)
|
||||
}
|
||||
|
||||
return checkoutToCart({ checkout })
|
||||
}
|
||||
|
||||
export function extendHook(
|
||||
customFetcher: typeof fetcher,
|
||||
swrOptions?: SwrOptions<Cart | null, CartInput>
|
||||
) {
|
||||
const useCart = () => {
|
||||
const response = useCommerceCart(defaultOpts, [], customFetcher, {
|
||||
revalidateOnFocus: false,
|
||||
...swrOptions,
|
||||
})
|
||||
const res = useResponse(response, {
|
||||
descriptors: {
|
||||
isEmpty: {
|
||||
get() {
|
||||
return (response.data?.lineItems.length ?? 0) <= 0
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
useCart.extend = extendHook
|
||||
|
||||
return useCart
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
72
framework/shopify/cart/use-remove-item.tsx
Normal file
72
framework/shopify/cart/use-remove-item.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
import { useCallback } from 'react'
|
||||
import { HookFetcher } from '@commerce/utils/types'
|
||||
import { ValidationError } from '@commerce/utils/errors'
|
||||
import useCartRemoveItem, {
|
||||
RemoveItemInput as UseRemoveItemInput,
|
||||
} from '@commerce/cart/use-remove-item'
|
||||
|
||||
import useCart from './use-cart'
|
||||
|
||||
import type { Cart, LineItem, RemoveCartItemBody } from '@commerce/types'
|
||||
import { checkoutLineItemRemoveMutation } from '@framework/utils/mutations'
|
||||
import getCheckoutId from '@framework/utils/get-checkout-id'
|
||||
import { checkoutToCart } from './utils'
|
||||
|
||||
const defaultOpts = {
|
||||
query: checkoutLineItemRemoveMutation,
|
||||
}
|
||||
|
||||
export type RemoveItemFn<T = any> = T extends LineItem
|
||||
? (input?: RemoveItemInput<T>) => Promise<Cart | null>
|
||||
: (input: RemoveItemInput<T>) => Promise<Cart | null>
|
||||
|
||||
export type RemoveItemInput<T = any> = T extends LineItem
|
||||
? Partial<UseRemoveItemInput>
|
||||
: UseRemoveItemInput
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, any> = async (
|
||||
options,
|
||||
{ itemId, checkoutId },
|
||||
fetch
|
||||
) => {
|
||||
const data = await fetch<any>({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
variables: { lineItemIds: [itemId], checkoutId },
|
||||
})
|
||||
return checkoutToCart(data?.checkoutLineItemsRemove)
|
||||
}
|
||||
|
||||
export function extendHook(customFetcher: typeof fetcher) {
|
||||
const useRemoveItem = <T extends LineItem | undefined = undefined>(
|
||||
item?: T
|
||||
) => {
|
||||
const { mutate, data: cart } = useCart()
|
||||
const fn = useCartRemoveItem<Cart | null, any>(defaultOpts, customFetcher)
|
||||
const removeItem: RemoveItemFn<LineItem> = async (input) => {
|
||||
const itemId = input?.id ?? item?.id
|
||||
|
||||
if (!itemId) {
|
||||
throw new ValidationError({
|
||||
message: 'Invalid input used for this operation',
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fn({
|
||||
checkoutId: getCheckoutId(cart?.id),
|
||||
itemId,
|
||||
})
|
||||
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}
|
||||
|
||||
return useCallback(removeItem as RemoveItemFn<T>, [fn, mutate])
|
||||
}
|
||||
|
||||
useRemoveItem.extend = extendHook
|
||||
|
||||
return useRemoveItem
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
85
framework/shopify/cart/use-update-item.tsx
Normal file
85
framework/shopify/cart/use-update-item.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useCallback } from 'react'
|
||||
import debounce from 'lodash.debounce'
|
||||
import type { HookFetcher } from '@commerce/utils/types'
|
||||
import { ValidationError } from '@commerce/utils/errors'
|
||||
import useCartUpdateItem, {
|
||||
UpdateItemInput as UseUpdateItemInput,
|
||||
} from '@commerce/cart/use-update-item'
|
||||
|
||||
import { fetcher as removeFetcher } from './use-remove-item'
|
||||
|
||||
import useCart from './use-cart'
|
||||
|
||||
import type { Cart, LineItem, UpdateCartItemBody } from '@commerce/types'
|
||||
import { checkoutToCart } from './utils'
|
||||
import checkoutLineItemUpdateMutation from '@framework/utils/mutations/checkout-line-item-update'
|
||||
import getCheckoutId from '@framework/utils/get-checkout-id'
|
||||
|
||||
const defaultOpts = {
|
||||
query: checkoutLineItemUpdateMutation,
|
||||
}
|
||||
|
||||
export type UpdateItemInput<T = any> = T extends LineItem
|
||||
? Partial<UseUpdateItemInput<LineItem>>
|
||||
: UseUpdateItemInput<LineItem>
|
||||
|
||||
export const fetcher: HookFetcher<Cart | null, any> = async (
|
||||
options,
|
||||
{ item, checkoutId },
|
||||
fetch
|
||||
) => {
|
||||
if (Number.isInteger(item.quantity)) {
|
||||
// Also allow the update hook to remove an item if the quantity is lower than 1
|
||||
if (item.quantity! < 1) {
|
||||
return removeFetcher(null, { itemId: item.id, checkoutId }, fetch)
|
||||
}
|
||||
} else if (item.quantity) {
|
||||
throw new ValidationError({
|
||||
message: 'The item quantity has to be a valid integer',
|
||||
})
|
||||
}
|
||||
const data = await fetch<any, any>({
|
||||
...defaultOpts,
|
||||
...options,
|
||||
variables: { checkoutId, lineItems: [item] },
|
||||
})
|
||||
|
||||
return checkoutToCart(data?.checkoutLineItemsUpdate)
|
||||
}
|
||||
|
||||
function extendHook(customFetcher: typeof fetcher, cfg?: { wait?: number }) {
|
||||
const useUpdateItem = <T extends LineItem | undefined = undefined>(
|
||||
item?: T
|
||||
) => {
|
||||
const { mutate, data: cart } = useCart()
|
||||
const fn = useCartUpdateItem<Cart | null, any>(defaultOpts, customFetcher)
|
||||
|
||||
return useCallback(
|
||||
debounce(async (input: UpdateItemInput<T>) => {
|
||||
const itemId = input.id ?? item?.id
|
||||
const variantId = input.productId ?? item?.variantId
|
||||
|
||||
if (!itemId || !variantId) {
|
||||
throw new ValidationError({
|
||||
message: 'Invalid input used for this operation',
|
||||
})
|
||||
}
|
||||
|
||||
const data = await fn({
|
||||
item: { id: itemId, variantId, quantity: input.quantity },
|
||||
checkoutId: getCheckoutId(cart?.id),
|
||||
})
|
||||
|
||||
await mutate(data, false)
|
||||
return data
|
||||
}, cfg?.wait ?? 500),
|
||||
[fn, mutate]
|
||||
)
|
||||
}
|
||||
|
||||
useUpdateItem.extend = extendHook
|
||||
|
||||
return useUpdateItem
|
||||
}
|
||||
|
||||
export default extendHook(fetcher)
|
20
framework/shopify/cart/utils/checkout-create.ts
Normal file
20
framework/shopify/cart/utils/checkout-create.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { SHOPIFY_CHECKOUT_COOKIE } from '@framework'
|
||||
import checkoutCreateMutation from '@framework/utils/mutations/checkout-create'
|
||||
import Cookies from 'js-cookie'
|
||||
|
||||
export const createCheckout = async (fetch: any) => {
|
||||
const data = await fetch({
|
||||
query: checkoutCreateMutation,
|
||||
})
|
||||
|
||||
const checkout = data?.checkoutCreate?.checkout
|
||||
const checkoutId = checkout?.id
|
||||
|
||||
if (checkoutId) {
|
||||
Cookies.set(SHOPIFY_CHECKOUT_COOKIE, checkoutId)
|
||||
}
|
||||
|
||||
return checkout
|
||||
}
|
||||
|
||||
export default createCheckout
|
57
framework/shopify/cart/utils/checkout-to-cart.ts
Normal file
57
framework/shopify/cart/utils/checkout-to-cart.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { Cart } from '@commerce/types'
|
||||
import { CommerceError, ValidationError } from '@commerce/utils/errors'
|
||||
import { Checkout, CheckoutLineItemEdge, Maybe } from '@framework/schema'
|
||||
|
||||
const checkoutToCart = (checkoutResponse?: any): Maybe<Cart> => {
|
||||
if (!checkoutResponse) {
|
||||
throw new CommerceError({
|
||||
message: 'Missing checkout details from response cart Response',
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
checkout,
|
||||
userErrors,
|
||||
}: { checkout?: Checkout; userErrors?: any[] } = checkoutResponse
|
||||
|
||||
if (userErrors && userErrors.length) {
|
||||
throw new ValidationError({
|
||||
message: userErrors[0].message,
|
||||
})
|
||||
}
|
||||
|
||||
if (!checkout) {
|
||||
throw new ValidationError({
|
||||
message: 'Missing checkout details from response cart Response',
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
...checkout,
|
||||
currency: { code: checkout.currencyCode },
|
||||
lineItems: checkout.lineItems?.edges.map(
|
||||
({
|
||||
node: { id, title: name, quantity, variant },
|
||||
}: CheckoutLineItemEdge) => ({
|
||||
id,
|
||||
checkoutUrl: checkout.webUrl,
|
||||
variantId: variant?.id,
|
||||
productId: id,
|
||||
name,
|
||||
quantity,
|
||||
discounts: [],
|
||||
path: '',
|
||||
variant: {
|
||||
id: variant?.id,
|
||||
image: {
|
||||
url: variant?.image?.src,
|
||||
altText: variant?.title,
|
||||
},
|
||||
price: variant?.price,
|
||||
},
|
||||
})
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
export default checkoutToCart
|
2
framework/shopify/cart/utils/index.ts
Normal file
2
framework/shopify/cart/utils/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as checkoutToCart } from './checkout-to-cart'
|
||||
export { default as checkoutCreate } from './checkout-create'
|
Reference in New Issue
Block a user