Implement Shopify Provider

This commit is contained in:
cond0r
2021-02-04 13:23:44 +02:00
parent c06d9dae3a
commit 14c3f961b3
61 changed files with 16405 additions and 20 deletions

View 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'

View 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)

View 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)

View 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)

View 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)

View 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

View 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

View File

@@ -0,0 +1,2 @@
export { default as checkoutToCart } from './checkout-to-cart'
export { default as checkoutCreate } from './checkout-create'