Implements custom checkout along with ordercloud provider

This commit is contained in:
goncy 2021-09-14 15:37:17 -03:00
parent a7c13d2d42
commit 30744c5a6c
67 changed files with 1886 additions and 174 deletions

View File

@ -25,3 +25,4 @@ NEXT_PUBLIC_VENDURE_SHOP_API_URL=
NEXT_PUBLIC_VENDURE_LOCAL_URL=
NEXT_PUBLIC_ORDERCLOUD_CLIENT_ID=
STRIPE_SECRET=

View File

@ -1,4 +1,3 @@
import cn from 'classnames'
import Link from 'next/link'
import { FC } from 'react'
import CartItem from '@components/cart/CartItem'
@ -7,24 +6,34 @@ import { useUI } from '@components/ui/context'
import useCart from '@framework/cart/use-cart'
import usePrice from '@framework/product/use-price'
import ShippingWidget from '../ShippingWidget'
import useCheckout from '@framework/checkout/use-checkout'
import PaymentWidget from '../PaymentWidget'
import SidebarLayout from '@components/common/SidebarLayout'
import s from './CheckoutSidebarView.module.css'
const CheckoutSidebarView: FC = () => {
const { setSidebarView } = useUI()
const { data } = useCart()
const { setSidebarView, closeSidebar } = useUI()
const { data: cartData } = useCart()
const { data: checkoutData, submit: onCheckout } = useCheckout();
async function handleSubmit(event: React.ChangeEvent<HTMLFormElement>) {
event.preventDefault();
await onCheckout();
closeSidebar();
}
const { price: subTotal } = usePrice(
data && {
amount: Number(data.subtotalPrice),
currencyCode: data.currency.code,
cartData && {
amount: Number(cartData.subtotalPrice),
currencyCode: cartData.currency.code,
}
)
const { price: total } = usePrice(
data && {
amount: Number(data.totalPrice),
currencyCode: data.currency.code,
cartData && {
amount: Number(cartData.totalPrice),
currencyCode: cartData.currency.code,
}
)
@ -38,22 +47,22 @@ const CheckoutSidebarView: FC = () => {
<Text variant="sectionHeading">Checkout</Text>
</Link>
<PaymentWidget onClick={() => setSidebarView('PAYMENT_VIEW')} />
<ShippingWidget onClick={() => setSidebarView('SHIPPING_VIEW')} />
<PaymentWidget isValid={checkoutData?.hasPayment} onClick={() => setSidebarView('PAYMENT_VIEW')} />
<ShippingWidget isValid={checkoutData?.hasShipping} onClick={() => setSidebarView('SHIPPING_VIEW')} />
<ul className={s.lineItemsList}>
{data!.lineItems.map((item: any) => (
{cartData!.lineItems.map((item: any) => (
<CartItem
key={item.id}
item={item}
currencyCode={data!.currency.code}
currencyCode={cartData!.currency.code}
variant="display"
/>
))}
</ul>
</div>
<div className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accent-0 border-t text-sm">
<form onSubmit={handleSubmit} className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accent-0 border-t text-sm">
<ul className="pb-2">
<li className="flex justify-between py-1">
<span>Subtotal</span>
@ -74,14 +83,11 @@ const CheckoutSidebarView: FC = () => {
</div>
<div>
{/* Once data is correcly filled */}
{/* <Button Component="a" width="100%">
<Button type="submit" width="100%" disabled={!checkoutData?.hasPayment || !checkoutData?.hasShipping}>
Confirm Purchase
</Button> */}
<Button Component="a" width="100%" variant="ghost" disabled>
Continue
</Button>
</div>
</div>
</form>
</SidebarLayout>
)
}

View File

@ -1,83 +1,123 @@
import { FC } from 'react'
import cn from 'classnames'
import useCheckout from '@framework/checkout/use-checkout'
import { Button, Text } from '@components/ui'
import { useUI } from '@components/ui/context'
import s from './PaymentMethodView.module.css'
import SidebarLayout from '@components/common/SidebarLayout'
import s from './PaymentMethodView.module.css'
interface Form extends HTMLFormElement {
cardHolder: HTMLInputElement
cardNumber: HTMLInputElement
cardExpireDate: HTMLInputElement
cardCvc: HTMLInputElement
firstName: HTMLInputElement
lastName: HTMLInputElement
company: HTMLInputElement
streetNumber: HTMLInputElement
zipCode: HTMLInputElement
city: HTMLInputElement
country: HTMLSelectElement
}
const PaymentMethodView: FC = () => {
const { setSidebarView } = useUI()
const [, {addPayment}] = useCheckout()
async function handleSubmit(event: React.ChangeEvent<Form>) {
event.preventDefault();
await addPayment({
cardHolder: event.target.cardHolder.value,
cardNumber: event.target.cardNumber.value,
cardExpireDate: event.target.cardExpireDate.value,
cardCvc: event.target.cardCvc.value,
firstName: event.target.firstName.value,
lastName: event.target.lastName.value,
company: event.target.company.value,
streetNumber: event.target.streetNumber.value,
zipCode: event.target.zipCode.value,
city: event.target.city.value,
country: event.target.country.value
});
setSidebarView('CHECKOUT_VIEW')
}
return (
<form className="h-full" onSubmit={handleSubmit}>
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
<div className="px-4 sm:px-6 flex-1">
<Text variant="sectionHeading"> Payment Method</Text>
<div>
<div className={s.fieldset}>
<label className={s.label}>Cardholder Name</label>
<input className={s.input} />
<input name="cardHolder" className={s.input} />
</div>
<div className="grid gap-3 grid-flow-row grid-cols-12">
<div className={cn(s.fieldset, 'col-span-7')}>
<label className={s.label}>Card Number</label>
<input className={s.input} />
<input name="cardNumber" className={s.input} />
</div>
<div className={cn(s.fieldset, 'col-span-3')}>
<label className={s.label}>Expires</label>
<input className={s.input} placeholder="MM/YY" />
<input name="cardExpireDate" className={s.input} placeholder="MM/YY" />
</div>
<div className={cn(s.fieldset, 'col-span-2')}>
<label className={s.label}>CVC</label>
<input className={s.input} />
<input name="cardCvc" className={s.input} />
</div>
</div>
<hr className="border-accent-2 my-6" />
<div className="grid gap-3 grid-flow-row grid-cols-12">
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>First Name</label>
<input className={s.input} />
<input name="firstName" className={s.input} />
</div>
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>Last Name</label>
<input className={s.input} />
<input name="lastName" className={s.input} />
</div>
</div>
<div className={s.fieldset}>
<label className={s.label}>Company (Optional)</label>
<input className={s.input} />
<input name="company" className={s.input} />
</div>
<div className={s.fieldset}>
<label className={s.label}>Street and House Number</label>
<input className={s.input} />
<input name="streetNumber" className={s.input} />
</div>
<div className={s.fieldset}>
<label className={s.label}>Apartment, Suite, Etc. (Optional)</label>
<input className={s.input} />
<input className={s.input} name="apartment" />
</div>
<div className="grid gap-3 grid-flow-row grid-cols-12">
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>Postal Code</label>
<input className={s.input} />
<input name="zipCode" className={s.input} />
</div>
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>City</label>
<input className={s.input} />
<input name="city" className={s.input} />
</div>
</div>
<div className={s.fieldset}>
<label className={s.label}>Country/Region</label>
<select className={s.select}>
<select name="country" className={s.select}>
<option>Hong Kong</option>
</select>
</div>
</div>
</div>
<div className="sticky z-20 bottom-0 w-full right-0 left-0 py-12 bg-accent-0 border-t border-accent-2 px-6">
<Button Component="a" width="100%" variant="ghost">
<Button type="submit" width="100%" variant="ghost">
Continue
</Button>
</div>
</SidebarLayout>
</form>
)
}

View File

@ -1,12 +1,13 @@
import { FC } from 'react'
import s from './PaymentWidget.module.css'
import { ChevronRight, CreditCard } from '@components/icons'
import { ChevronRight, CreditCard, Check } from '@components/icons'
interface ComponentProps {
onClick?: () => any
onClick?: () => any;
isValid?: boolean;
}
const PaymentWidget: FC<ComponentProps> = ({ onClick }) => {
const PaymentWidget: FC<ComponentProps> = ({ onClick, isValid }) => {
/* Shipping Address
Only available with checkout set to true -
This means that the provider does offer checkout functionality. */
@ -20,7 +21,7 @@ const PaymentWidget: FC<ComponentProps> = ({ onClick }) => {
{/* <span>VISA #### #### #### 2345</span> */}
</div>
<div>
<ChevronRight />
{isValid ? <Check /> : <ChevronRight />}
</div>
</div>
)

View File

@ -1,14 +1,51 @@
import { FC } from 'react'
import cn from 'classnames'
import s from './ShippingView.module.css'
import Button from '@components/ui/Button'
import { useUI } from '@components/ui/context'
import SidebarLayout from '@components/common/SidebarLayout'
import useCheckout from '@framework/checkout/use-checkout'
import s from './ShippingView.module.css'
interface Form extends HTMLFormElement {
cardHolder: HTMLInputElement
cardNumber: HTMLInputElement
cardExpireDate: HTMLInputElement
cardCvc: HTMLInputElement
firstName: HTMLInputElement
lastName: HTMLInputElement
company: HTMLInputElement
streetNumber: HTMLInputElement
zipCode: HTMLInputElement
city: HTMLInputElement
country: HTMLSelectElement
}
const PaymentMethodView: FC = () => {
const { setSidebarView } = useUI()
const [, {addAddress}] = useCheckout()
async function handleSubmit(event: React.ChangeEvent<Form>) {
event.preventDefault();
await addAddress({
type: event.target.type.value,
firstName: event.target.firstName.value,
lastName: event.target.lastName.value,
company: event.target.company.value,
streetNumber: event.target.streetNumber.value,
apartments: event.target.streetNumber.value,
zipCode: event.target.zipCode.value,
city: event.target.city.value,
country: event.target.country.value
});
setSidebarView('CHECKOUT_VIEW')
}
return (
<form className="h-full" onSubmit={handleSubmit}>
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
<div className="px-4 sm:px-6 flex-1">
<h2 className="pt-1 pb-8 text-2xl font-semibold tracking-wide cursor-pointer inline-block">
@ -16,11 +53,11 @@ const PaymentMethodView: FC = () => {
</h2>
<div>
<div className="flex flex-row my-3 items-center">
<input className={s.radio} type="radio" />
<input name="type" className={s.radio} type="radio" />
<span className="ml-3 text-sm">Same as billing address</span>
</div>
<div className="flex flex-row my-3 items-center">
<input className={s.radio} type="radio" />
<input name="type" className={s.radio} type="radio" />
<span className="ml-3 text-sm">
Use a different shipping address
</span>
@ -29,49 +66,50 @@ const PaymentMethodView: FC = () => {
<div className="grid gap-3 grid-flow-row grid-cols-12">
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>First Name</label>
<input className={s.input} />
<input name="firstName" className={s.input} />
</div>
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>Last Name</label>
<input className={s.input} />
<input name="lastName" className={s.input} />
</div>
</div>
<div className={s.fieldset}>
<label className={s.label}>Company (Optional)</label>
<input className={s.input} />
<input name="company" className={s.input} />
</div>
<div className={s.fieldset}>
<label className={s.label}>Street and House Number</label>
<input className={s.input} />
<input name="streetNumber" className={s.input} />
</div>
<div className={s.fieldset}>
<label className={s.label}>Apartment, Suite, Etc. (Optional)</label>
<input className={s.input} />
<input name="apartments" className={s.input} />
</div>
<div className="grid gap-3 grid-flow-row grid-cols-12">
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>Postal Code</label>
<input className={s.input} />
<input name="zipCode" className={s.input} />
</div>
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>City</label>
<input className={s.input} />
<input name="city" className={s.input} />
</div>
</div>
<div className={s.fieldset}>
<label className={s.label}>Country/Region</label>
<select className={s.select}>
<select name="country" className={s.select}>
<option>Hong Kong</option>
</select>
</div>
</div>
</div>
<div className="sticky z-20 bottom-0 w-full right-0 left-0 py-12 bg-accent-0 border-t border-accent-2 px-6">
<Button Component="a" width="100%" variant="ghost">
<Button type="submit" width="100%" variant="ghost">
Continue
</Button>
</div>
</SidebarLayout>
</form>
)
}

View File

@ -1,13 +1,14 @@
import { FC } from 'react'
import s from './ShippingWidget.module.css'
import { ChevronRight, MapPin } from '@components/icons'
import { ChevronRight, MapPin, Check } from '@components/icons'
import cn from 'classnames'
interface ComponentProps {
onClick?: () => any
isValid?: boolean;
}
const ShippingWidget: FC<ComponentProps> = ({ onClick }) => {
const ShippingWidget: FC<ComponentProps> = ({ onClick, isValid }) => {
/* Shipping Address
Only available with checkout set to true -
This means that the provider does offer checkout functionality. */
@ -24,7 +25,7 @@ const ShippingWidget: FC<ComponentProps> = ({ onClick }) => {
</span> */}
</div>
<div>
<ChevronRight />
{isValid ? <Check /> : <ChevronRight />}
</div>
</div>
)

View File

@ -1,25 +1,39 @@
import type { CheckoutSchema } from '../../types/checkout'
import type { GetAPISchema } from '..'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
const checkoutEndpoint: GetAPISchema<
any,
CheckoutSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers } = ctx
const { req, res, handlers, config } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['checkout'],
GET: handlers['getCheckout'],
POST: handlers['submitCheckout'],
})
) {
return
}
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
const body = null
return await handlers['checkout']({ ...ctx, body })
// Create checkout
if (req.method === 'GET') {
const body = { ...req.body, cartId }
return await handlers['getCheckout']({ ...ctx, body })
}
// Create checkout
if (req.method === 'POST') {
const body = { ...req.body, cartId }
return await handlers['submitCheckout']({ ...ctx, body })
}
} catch (error) {
console.error(error)

View File

@ -0,0 +1,63 @@
import type { CustomerAddressSchema } from '../../../types/customer/address'
import type { GetAPISchema } from '../..'
import { CommerceAPIError } from '../../utils/errors'
import isAllowedOperation from '../../utils/is-allowed-operation'
const customerShippingEndpoint: GetAPISchema<
any,
CustomerAddressSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['getAddresses'],
POST: handlers['addItem'],
PUT: handlers['updateItem'],
DELETE: handlers['removeItem'],
})
) {
return
}
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
// Return current cart info
if (req.method === 'GET') {
const body = { cartId }
return await handlers['getAddresses']({ ...ctx, body })
}
// Create or add an item to the cart
if (req.method === 'POST') {
const body = { ...req.body, cartId }
return await handlers['addItem']({ ...ctx, body })
}
// Update item in cart
if (req.method === 'PUT') {
const body = { ...req.body, cartId }
return await handlers['updateItem']({ ...ctx, body })
}
// Remove an item from the cart
if (req.method === 'DELETE') {
const body = { ...req.body, cartId }
return await handlers['removeItem']({ ...ctx, body })
}
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default customerShippingEndpoint

View File

@ -0,0 +1,63 @@
import type { CustomerCardSchema } from '../../../types/customer/card'
import type { GetAPISchema } from '../..'
import { CommerceAPIError } from '../../utils/errors'
import isAllowedOperation from '../../utils/is-allowed-operation'
const customerCardEndpoint: GetAPISchema<
any,
CustomerCardSchema
>['endpoint']['handler'] = async (ctx) => {
const { req, res, handlers, config } = ctx
if (
!isAllowedOperation(req, res, {
GET: handlers['getCards'],
POST: handlers['addItem'],
PUT: handlers['updateItem'],
DELETE: handlers['removeItem'],
})
) {
return
}
const { cookies } = req
const cartId = cookies[config.cartCookie]
try {
// Create or add a payment
if (req.method === 'GET') {
const body = { ...req.body }
return await handlers['getCards']({ ...ctx, body })
}
// Create or add an item to the cart
if (req.method === 'POST') {
const body = { ...req.body, cartId }
return await handlers['addItem']({ ...ctx, body })
}
// Update item in cart
if (req.method === 'PUT') {
const body = { ...req.body, cartId }
return await handlers['updateItem']({ ...ctx, body })
}
// Remove an item from the cart
if (req.method === 'DELETE') {
const body = { ...req.body, cartId }
return await handlers['removeItem']({ ...ctx, body })
}
} catch (error) {
console.error(error)
const message =
error instanceof CommerceAPIError
? 'An unexpected error ocurred with the Commerce API'
: 'An unexpected error ocurred'
res.status(500).json({ data: null, errors: [{ message }] })
}
}
export default customerCardEndpoint

View File

@ -1,7 +1,7 @@
import type { CustomerSchema } from '../../types/customer'
import { CommerceAPIError } from '../utils/errors'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..'
import type { CustomerSchema } from '../../../types/customer'
import { CommerceAPIError } from '../../utils/errors'
import isAllowedOperation from '../../utils/is-allowed-operation'
import type { GetAPISchema } from '../..'
const customerEndpoint: GetAPISchema<
any,

View File

@ -9,6 +9,8 @@ import type { SignupSchema } from '../types/signup'
import type { ProductsSchema } from '../types/product'
import type { WishlistSchema } from '../types/wishlist'
import type { CheckoutSchema } from '../types/checkout'
import type { CustomerCardSchema } from '../types/customer/card'
import type { CustomerAddressSchema } from '../types/customer/address'
import {
defaultOperations,
OPERATIONS,
@ -25,6 +27,8 @@ export type APISchemas =
| ProductsSchema
| WishlistSchema
| CheckoutSchema
| CustomerCardSchema
| CustomerAddressSchema
export type GetAPISchema<
C extends CommerceAPI<any>,

View File

@ -0,0 +1,19 @@
import useGetCheckout from "./use-get-checkout"
import useSubmitCheckout from "./use-submit-checkout";
import useAddPayment from "../customer/card/use-add-item"
import useAddShipping from "../customer/address/use-add-item"
export type UseCheckout = any;
function useCheckout(): UseCheckout {
const state = useGetCheckout()
const actions = {
submit: useSubmitCheckout(),
addPayment: useAddPayment(),
addShipping: useAddShipping()
}
return {...state, ...actions}
}
export default useCheckout

View File

@ -0,0 +1,34 @@
import type { SWRHook, HookFetcherFn } from '../utils/types'
import type { GetCheckoutHook } from '../types/checkout'
import Cookies from 'js-cookie'
import { useHook, useSWRHook } from '../utils/use-hook'
import { Provider, useCommerce } from '..'
export type UseGetCheckout<
H extends SWRHook<GetCheckoutHook<any>> = SWRHook<GetCheckoutHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<GetCheckoutHook> = async ({
options,
input: { cartId },
fetch,
}) => {
return cartId ? await fetch(options) : null
}
const fn = (provider: Provider) => provider.checkout?.useGetCheckout!
const useGetCheckout: UseGetCheckout = (input) => {
const hook = useHook(fn)
const { cartCookie } = useCommerce()
const fetcherFn = hook.fetcher ?? fetcher
const wrapper: typeof fetcher = (context) => {
context.input.cartId = Cookies.get(cartCookie)
return fetcherFn(context)
}
return useSWRHook({ ...hook, fetcher: wrapper })(input)
}
export default useGetCheckout

View File

@ -0,0 +1,21 @@
import type { HookFetcherFn, MutationHook } from '../utils/types'
import type { SubmitCheckoutHook } from '../types/checkout'
import type { Provider } from '..'
import { useHook, useMutationHook } from '../utils/use-hook'
import { mutationFetcher } from '../utils/default-fetcher'
export type UseSubmitCheckout<
H extends MutationHook<SubmitCheckoutHook<any>> = MutationHook<SubmitCheckoutHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<SubmitCheckoutHook> = mutationFetcher
const fn = (provider: Provider) => provider.checkout?.useSubmitCheckout!
const useSubmitCheckout: UseSubmitCheckout = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useSubmitCheckout

View File

@ -0,0 +1,21 @@
import type { HookFetcherFn, MutationHook } from '../../utils/types'
import type { AddItemHook } from '../../types/customer/address'
import type { Provider } from '../..'
import { useHook, useMutationHook } from '../../utils/use-hook'
import { mutationFetcher } from '../../utils/default-fetcher'
export type UseAddItem<
H extends MutationHook<AddItemHook<any>> = MutationHook<AddItemHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<AddItemHook> = mutationFetcher
const fn = (provider: Provider) => provider.customer?.address?.useAddItem!
const useAddItem: UseAddItem = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useAddItem

View File

@ -0,0 +1,32 @@
import Cookies from 'js-cookie'
import { useHook, useSWRHook } from '../../utils/use-hook'
import type { SWRHook, HookFetcherFn } from '../../utils/types'
import type { GetAddressesHook } from '../../types/customer/address'
import { Provider, useCommerce } from '../..'
export type UseAddresses<
H extends SWRHook<GetAddressesHook<any>> = SWRHook<GetAddressesHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<GetAddressesHook> = async ({
options,
input: { cartId },
fetch,
}) => {
return cartId ? await fetch(options) : null
}
const fn = (provider: Provider) => provider.customer?.address.useAddresses!
const useAddresses: UseAddresses = (input) => {
const hook = useHook(fn)
const { cartCookie } = useCommerce()
const fetcherFn = hook.fetcher ?? fetcher
const wrapper: typeof fetcher = (context) => {
context.input.cartId = Cookies.get(cartCookie)
return fetcherFn(context)
}
return useSWRHook({ ...hook, fetcher: wrapper })(input)
}
export default useAddresses

View File

@ -0,0 +1,20 @@
import { useHook, useMutationHook } from '../../utils/use-hook'
import { mutationFetcher } from '../../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../../utils/types'
import type { RemoveItemHook } from '../../types/customer/address'
import type { Provider } from '../..'
export type UseRemoveItem<
H extends MutationHook<RemoveItemHook<any>> = MutationHook<RemoveItemHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<RemoveItemHook> = mutationFetcher
const fn = (provider: Provider) => provider.customer?.address?.useRemoveItem!
const useRemoveItem: UseRemoveItem = (input) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(input)
}
export default useRemoveItem

View File

@ -0,0 +1,20 @@
import { useHook, useMutationHook } from '../../utils/use-hook'
import { mutationFetcher } from '../../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../../utils/types'
import type { UpdateItemHook } from '../../types/customer/address'
import type { Provider } from '../..'
export type UseUpdateItem<
H extends MutationHook<UpdateItemHook<any>> = MutationHook<UpdateItemHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<UpdateItemHook> = mutationFetcher
const fn = (provider: Provider) => provider?.customer?.address?.useUpdateItem!
const useUpdateItem: UseUpdateItem = (input) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(input)
}
export default useUpdateItem

View File

@ -0,0 +1,21 @@
import type { HookFetcherFn, MutationHook } from '../../utils/types'
import type { AddItemHook } from '../../types/customer/card'
import type { Provider } from '../..'
import { useHook, useMutationHook } from '../../utils/use-hook'
import { mutationFetcher } from '../../utils/default-fetcher'
export type UseAddItem<
H extends MutationHook<AddItemHook<any>> = MutationHook<AddItemHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<AddItemHook> = mutationFetcher
const fn = (provider: Provider) => provider.customer?.card?.useAddItem!
const useAddItem: UseAddItem = (...args) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(...args)
}
export default useAddItem

View File

@ -0,0 +1,32 @@
import Cookies from 'js-cookie'
import { useHook, useSWRHook } from '../../utils/use-hook'
import type { SWRHook, HookFetcherFn } from '../../utils/types'
import type { GetCardsHook } from '../../types/customer/card'
import { Provider, useCommerce } from '../..'
export type UseCards<
H extends SWRHook<GetCardsHook<any>> = SWRHook<GetCardsHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<GetCardsHook> = async ({
options,
input: { cartId },
fetch,
}) => {
return cartId ? await fetch(options) : null
}
const fn = (provider: Provider) => provider.customer?.card.useCards!
const useCards: UseCards = (input) => {
const hook = useHook(fn)
const { cartCookie } = useCommerce()
const fetcherFn = hook.fetcher ?? fetcher
const wrapper: typeof fetcher = (context) => {
context.input.cartId = Cookies.get(cartCookie)
return fetcherFn(context)
}
return useSWRHook({ ...hook, fetcher: wrapper })(input)
}
export default useCards

View File

@ -0,0 +1,20 @@
import { useHook, useMutationHook } from '../../utils/use-hook'
import { mutationFetcher } from '../../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../../utils/types'
import type { RemoveItemHook } from '../../types/customer/card'
import type { Provider } from '../..'
export type UseRemoveItem<
H extends MutationHook<RemoveItemHook<any>> = MutationHook<RemoveItemHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<RemoveItemHook> = mutationFetcher
const fn = (provider: Provider) => provider.customer?.card?.useRemoveItem!
const useRemoveItem: UseRemoveItem = (input) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(input)
}
export default useRemoveItem

View File

@ -0,0 +1,20 @@
import { useHook, useMutationHook } from '../../utils/use-hook'
import { mutationFetcher } from '../../utils/default-fetcher'
import type { HookFetcherFn, MutationHook } from '../../utils/types'
import type { UpdateItemHook } from '../../types/customer/card'
import type { Provider } from '../..'
export type UseUpdateItem<
H extends MutationHook<UpdateItemHook<any>> = MutationHook<UpdateItemHook>
> = ReturnType<H['useHook']>
export const fetcher: HookFetcherFn<UpdateItemHook> = mutationFetcher
const fn = (provider: Provider) => provider?.customer?.card?.useUpdateItem!
const useUpdateItem: UseUpdateItem = (input) => {
const hook = useHook(fn)
return useMutationHook({ fetcher, ...hook })(input)
}
export default useUpdateItem

View File

@ -15,6 +15,7 @@ import type {
Signup,
Login,
Logout,
Checkout,
} from '@commerce/types'
import type { Fetcher, SWRHook, MutationHook } from './utils/types'
@ -29,6 +30,11 @@ export type Provider = CommerceConfig & {
useUpdateItem?: MutationHook<Cart.UpdateItemHook>
useRemoveItem?: MutationHook<Cart.RemoveItemHook>
}
checkout?: {
useSubmitCheckout?: MutationHook<Checkout.SubmitCheckoutHook>
useGetCheckout?: SWRHook<Checkout.GetCheckoutHook>
useCheckout?: any;
}
wishlist?: {
useWishlist?: SWRHook<Wishlist.GetWishlistHook>
useAddItem?: MutationHook<Wishlist.AddItemHook>
@ -36,6 +42,18 @@ export type Provider = CommerceConfig & {
}
customer?: {
useCustomer?: SWRHook<Customer.CustomerHook>
card: {
useCards?: SWRHook<Customer.Card.GetCardsHook>
useAddItem?: MutationHook<Customer.Card.AddItemHook>
useUpdateItem?: MutationHook<Customer.Card.UpdateItemHook>
useRemoveItem?: MutationHook<Customer.Card.RemoveItemHook>
}
address: {
useAddresses?: SWRHook<Customer.Address.GetAddressesHook>
useAddItem?: MutationHook<Customer.Address.AddItemHook>
useUpdateItem?: MutationHook<Customer.Address.UpdateItemHook>
useRemoveItem?: MutationHook<Customer.Address.RemoveItemHook>
}
}
products?: {
useSearch?: SWRHook<Product.SearchProductsHook>

View File

@ -1,10 +1,48 @@
export type CheckoutSchema = {
// Index
export type CheckoutTypes = {
card?: any;
address?: any;
checkout?: any;
hasPayment?: boolean;
hasShipping?: boolean;
}
export type SubmitCheckoutHook<T extends CheckoutTypes = CheckoutTypes> = {
data: T
input?: T
fetcherInput: T
body: { item: T }
actionInput: T
}
export type GetCheckoutHook<T extends CheckoutTypes = CheckoutTypes> = {
data: T['checkout'] | null
input: {}
fetcherInput: { cartId?: string }
swrState: { isEmpty: boolean }
}
export type CheckoutHooks<T extends CheckoutTypes = CheckoutTypes> = {
submitCheckout: SubmitCheckoutHook<T>
getCheckout: GetCheckoutHook<T>
}
export type GetCheckoutHandler<T extends CheckoutTypes = CheckoutTypes> = GetCheckoutHook<T> & {
body: { cartId: string }
}
export type SubmitCheckoutHandler<T extends CheckoutTypes = CheckoutTypes> = SubmitCheckoutHook<T> & {
body: { cartId: string }
}
export type CheckoutHandlers<T extends CheckoutTypes = CheckoutTypes> = {
getCheckout: GetCheckoutHandler<T>
submitCheckout: SubmitCheckoutHandler<T>
}
export type CheckoutSchema<T extends CheckoutTypes = CheckoutTypes> = {
endpoint: {
options: {}
handlers: {
checkout: {
data: null
}
}
handlers: CheckoutHandlers<T>
}
}

View File

@ -0,0 +1,93 @@
export interface Address {
id: string;
mask: string;
}
export interface AddressFields {
type: string;
firstName: string;
lastName: string;
company: string;
streetNumber: string;
apartments: string;
zipCode: string;
city: string;
country: string;
}
export type CustomerAddressTypes = {
address?: Address;
fields: AddressFields;
}
export type GetAddressesHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
data: T['address'] | null
input: {}
fetcherInput: { cartId?: string }
swrState: { isEmpty: boolean }
}
export type AddItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
data: T['address']
input?: T['fields']
fetcherInput: T['fields']
body: { item: T['fields'] }
actionInput: T['fields']
}
export type UpdateItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
data: T['address'] | null
input: { item?: T['fields']; wait?: number }
fetcherInput: { itemId: string; item: T['fields'] }
body: { itemId: string; item: T['fields'] }
actionInput: T['fields'] & { id: string }
}
export type RemoveItemHook<T extends CustomerAddressTypes = CustomerAddressTypes> = {
data: T['address'] | null
input: { item?: T['fields'] }
fetcherInput: { itemId: string }
body: { itemId: string }
actionInput: { id: string }
}
export type CustomerAddressHooks<T extends CustomerAddressTypes = CustomerAddressTypes> = {
getAddresses: GetAddressesHook<T>
addItem: AddItemHook<T>
updateItem: UpdateItemHook<T>
removeItem: RemoveItemHook<T>
}
export type AddresssHandler<T extends CustomerAddressTypes = CustomerAddressTypes> = GetAddressesHook<T> & {
body: { cartId?: string }
}
export type AddItemHandler<T extends CustomerAddressTypes = CustomerAddressTypes> = AddItemHook<T> & {
body: { cartId: string }
}
export type UpdateItemHandler<T extends CustomerAddressTypes = CustomerAddressTypes> =
UpdateItemHook<T> & {
data: T['address']
body: { cartId: string }
}
export type RemoveItemHandler<T extends CustomerAddressTypes = CustomerAddressTypes> =
RemoveItemHook<T> & {
body: { cartId: string }
}
export type CustomerAddressHandlers<T extends CustomerAddressTypes = CustomerAddressTypes> = {
getAddresses: GetAddressesHook<T>
addItem: AddItemHandler<T>
updateItem: UpdateItemHandler<T>
removeItem: RemoveItemHandler<T>
}
export type CustomerAddressSchema<T extends CustomerAddressTypes = CustomerAddressTypes> = {
endpoint: {
options: {}
handlers: CustomerAddressHandlers<T>
}
}

View File

@ -0,0 +1,96 @@
export interface Card {
id: string;
mask: string;
provider: string;
}
export interface CardFields {
cardHolder: string;
cardNumber: string;
cardExpireDate: string;
cardCvc: string;
firstName: string;
lastName: string;
company: string;
streetNumber: string;
zipCode: string;
city: string;
country: string;
}
export type CustomerCardTypes = {
card?: Card;
fields: CardFields;
}
export type GetCardsHook<T extends CustomerCardTypes = CustomerCardTypes> = {
data: T['card'] | null
input: {}
fetcherInput: { cartId?: string }
swrState: { isEmpty: boolean }
}
export type AddItemHook<T extends CustomerCardTypes = CustomerCardTypes> = {
data: T['card']
input?: T['fields']
fetcherInput: T['fields']
body: { item: T['fields'] }
actionInput: T['fields']
}
export type UpdateItemHook<T extends CustomerCardTypes = CustomerCardTypes> = {
data: T['card'] | null
input: { item?: T['fields']; wait?: number }
fetcherInput: { itemId: string; item: T['fields'] }
body: { itemId: string; item: T['fields'] }
actionInput: T['fields'] & { id: string }
}
export type RemoveItemHook<T extends CustomerCardTypes = CustomerCardTypes> = {
data: T['card'] | null
input: { item?: T['fields'] }
fetcherInput: { itemId: string }
body: { itemId: string }
actionInput: { id: string }
}
export type CustomerCardHooks<T extends CustomerCardTypes = CustomerCardTypes> = {
getCards: GetCardsHook<T>
addItem: AddItemHook<T>
updateItem: UpdateItemHook<T>
removeItem: RemoveItemHook<T>
}
export type CardsHandler<T extends CustomerCardTypes = CustomerCardTypes> = GetCardsHook<T> & {
body: { cartId?: string }
}
export type AddItemHandler<T extends CustomerCardTypes = CustomerCardTypes> = AddItemHook<T> & {
body: { cartId: string }
}
export type UpdateItemHandler<T extends CustomerCardTypes = CustomerCardTypes> =
UpdateItemHook<T> & {
data: T['card']
body: { cartId: string }
}
export type RemoveItemHandler<T extends CustomerCardTypes = CustomerCardTypes> =
RemoveItemHook<T> & {
body: { cartId: string }
}
export type CustomerCardHandlers<T extends CustomerCardTypes = CustomerCardTypes> = {
getCards: GetCardsHook<T>
addItem: AddItemHandler<T>
updateItem: UpdateItemHandler<T>
removeItem: RemoveItemHandler<T>
}
export type CustomerCardSchema<T extends CustomerCardTypes = CustomerCardTypes> = {
endpoint: {
options: {}
handlers: CustomerCardHandlers<T>
}
}

View File

@ -1,3 +1,6 @@
export * as Card from "./card"
export * as Address from "./address"
// TODO: define this type
export type Customer = any

View File

@ -1 +1,3 @@
COMMERCE_PROVIDER=ordercloud
NEXT_PUBLIC_ORDERCLOUD_CLIENT_ID=

View File

@ -0,0 +1,35 @@
import type { CheckoutEndpoint } from '.'
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
res,
body: {cartId},
config: { restFetch },
}) => {
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing cookie' }],
})
}
// Register credit card
const payments = await restFetch('GET', `/orders/Outgoing/${cartId}/payments`).then(
(response: {Items: unknown[]}) => response.Items
)
const address = await restFetch('GET', `/orders/Outgoing/${cartId}`).then(
(response: {ShippingAddressID: string}) => response.ShippingAddressID
)
// Return cart and errors
res.status(200).json({
data: {
hasPayment: payments.length > 0,
hasShipping: Boolean(address)
},
errors: []
})
}
export default getCheckout

View File

@ -1 +1,23 @@
export default function noopApi(...args: any[]): void {}
import type { CheckoutSchema } from '../../../types/checkout'
import type { OrdercloudAPI } from '../..'
import { GetAPISchema, createEndpoint } from '@commerce/api'
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
import getCheckout from './get-checkout'
import submitCheckout from './submit-checkout'
export type CheckoutAPI = GetAPISchema<OrdercloudAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = {
getCheckout,
submitCheckout,
}
const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint,
handlers,
})
export default checkoutApi

View File

@ -0,0 +1,23 @@
import type { CheckoutEndpoint } from '.'
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
res,
body: { cartId },
config: { restFetch },
}) => {
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Submit order
await restFetch('POST', `/orders/Outgoing/${cartId}/submit`, {})
// Return cart and errors
res.status(200).json({ data: null, errors: [] })
}
export default submitCheckout

View File

@ -0,0 +1,49 @@
import type { CustomerAddressEndpoint } from '.'
const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
res,
body: { item, cartId },
config: { restFetch },
}) => {
// Return an error if no item is present
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Cookie not found' }],
})
}
// Register address
const address = await restFetch('POST', `/me/addresses`, {
"AddressName": "main address",
"CompanyName": item.company,
"FirstName": item.firstName,
"LastName": item.lastName,
"Street1": item.streetNumber,
"Street2": item.streetNumber,
"City": item.city,
"State": item.city,
"Zip": item.zipCode,
"Country": item.country.slice(0, 2).toLowerCase(),
"Shipping": true
}).then(
(response: {ID: string}) => response.ID
)
// Assign address to order
await restFetch('PATCH', `/orders/Outgoing/${cartId}`, {
ShippingAddressID: address
})
return res.status(200).json({ data: null, errors: [] })
}
export default addItem

View File

@ -0,0 +1,9 @@
import type { CustomerAddressEndpoint } from '.'
const getCards: CustomerAddressEndpoint['handlers']['getAddresses'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default getCards

View File

@ -0,0 +1,27 @@
import type { CustomerAddressSchema } from '../../../../types/customer/address'
import type { OrdercloudAPI } from '../../..'
import { GetAPISchema, createEndpoint } from '@commerce/api'
import customerAddressEndpoint from '@commerce/api/endpoints/customer/address'
import getAddresses from './get-addresses'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CustomerAddressAPI = GetAPISchema<OrdercloudAPI, CustomerAddressSchema>
export type CustomerAddressEndpoint = CustomerAddressAPI['endpoint']
export const handlers: CustomerAddressEndpoint['handlers'] = {
getAddresses,
addItem,
updateItem,
removeItem,
}
const customerAddressApi = createEndpoint<CustomerAddressAPI>({
handler: customerAddressEndpoint,
handlers,
})
export default customerAddressApi

View File

@ -0,0 +1,9 @@
import type { CustomerAddressEndpoint } from '.'
const removeItem: CustomerAddressEndpoint['handlers']['removeItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default removeItem

View File

@ -0,0 +1,9 @@
import type { CustomerAddressEndpoint } from '.'
const updateItem: CustomerAddressEndpoint['handlers']['updateItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default updateItem

View File

@ -0,0 +1,61 @@
import type { CustomerCardEndpoint } from '.'
import type { OredercloudCreditCard } from '../../../../types/customer/card'
import Stripe from "stripe"
const stripe = new Stripe(process.env.STRIPE_SECRET as string, {
apiVersion: "2020-08-27"
})
const addItem: CustomerCardEndpoint['handlers']['addItem'] = async ({
res,
body: { item, cartId },
config: { restFetch },
}) => {
// Return an error if no item is present
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Cookie not found' }],
})
}
// Get token
const token = await stripe.tokens.create({
card: {
number: item.cardNumber,
exp_month: item.cardExpireDate.split('/')[0],
exp_year: item.cardExpireDate.split('/')[1],
cvc: item.cardCvc
}
}).then((res: {id: string}) => res.id)
// Register credit card
const creditCard = await restFetch('POST', `/me/creditcards`, {
"Token": token,
"CardType": "credit",
"PartialAccountNumber": item.cardNumber.slice(-4),
"CardholderName": item.cardHolder,
"ExpirationDate": item.cardExpireDate
}).then(
(response: OredercloudCreditCard) => response.ID
)
// Assign payment to order
await restFetch('POST', `/orders/Outgoing/${cartId}/payments`, {
"Type": "CreditCard",
CreditCardID: creditCard
})
return res.status(200).json({ data: null, errors: [] })
}
export default addItem

View File

@ -0,0 +1,9 @@
import type { CustomerCardEndpoint } from '.'
const getCards: CustomerCardEndpoint['handlers']['getCards'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default getCards

View File

@ -0,0 +1,27 @@
import type { CustomerCardSchema } from '../../../../types/customer/card'
import type { OrdercloudAPI } from '../../..'
import { GetAPISchema, createEndpoint } from '@commerce/api'
import customerCardEndpoint from '@commerce/api/endpoints/customer/card'
import getCards from './get-cards'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CustomerCardAPI = GetAPISchema<OrdercloudAPI, CustomerCardSchema>
export type CustomerCardEndpoint = CustomerCardAPI['endpoint']
export const handlers: CustomerCardEndpoint['handlers'] = {
getCards,
addItem,
updateItem,
removeItem,
}
const customerCardApi = createEndpoint<CustomerCardAPI>({
handler: customerCardEndpoint,
handlers,
})
export default customerCardApi

View File

@ -0,0 +1,9 @@
import type { CustomerCardEndpoint } from '.'
const removeItem: CustomerCardEndpoint['handlers']['removeItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default removeItem

View File

@ -0,0 +1,9 @@
import type { CustomerCardEndpoint } from '.'
const updateItem: CustomerCardEndpoint['handlers']['updateItem'] = async ({
res,
}) => {
return res.status(200).json({ data: null, errors: [] })
}
export default updateItem

View File

@ -94,6 +94,8 @@ export async function fetchData<T>(
return fetchData(opts, retries + 1)
}
console.log('dataResponse.text:', await dataResponse.textConverted());
// Get the body of it
const error = await dataResponse.json()

View File

@ -0,0 +1,2 @@
export { default as useCheckout } from './use-submit-checkout'
export { default as useGetCheckout } from './use-get-checkout'

View File

@ -0,0 +1,6 @@
import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout'
export default useCheckout as UseCheckout
export const handler = useCheckout

View File

@ -0,0 +1,33 @@
import type { GetCheckoutHook } from '@commerce/types/checkout'
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useGetCheckout, { UseGetCheckout } from '@commerce/checkout/use-get-checkout'
export default useGetCheckout as UseGetCheckout<typeof handler>
export const handler: SWRHook<GetCheckoutHook> = {
fetchOptions: {
url: '/api/checkout',
method: 'GET',
},
useHook: ({ useData }) =>
function useHook(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]
)
},
}

View File

@ -0,0 +1,36 @@
import type { SubmitCheckoutHook } from '@commerce/types/checkout'
import type { MutationHook } from '@commerce/utils/types'
import { useCallback } from 'react'
import useSubmitCheckout, { UseSubmitCheckout } from '@commerce/checkout/use-submit-checkout'
export default useSubmitCheckout as UseSubmitCheckout<typeof handler>
export const handler: MutationHook<SubmitCheckoutHook> = {
fetchOptions: {
url: '/api/checkout',
method: 'POST',
},
async fetcher({ input: item, options, fetch }) {
// @TODO: Make form validations in here, import generic error like import { CommerceError } from '@commerce/utils/errors'
// Get payment and delivery information in here
const data = await fetch({
...options,
body: { item },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
return useCallback(
async function onSubmitCheckout(input) {
const data = await fetch({ input })
return data
},
[fetch]
)
},
}

View File

@ -5,6 +5,6 @@
"cart": true,
"search": false,
"customerAuth": false,
"customCheckout": false
"customCheckout": true
}
}

View File

@ -0,0 +1,4 @@
export { default as useAddresses } from './use-addresses'
export { default as useAddItem } from './use-add-item'
export { default as useRemoveItem } from './use-remove-item'
export { default as useUpdateItem } from './use-update-item'

View File

@ -0,0 +1,48 @@
import type { AddItemHook } from '@commerce/types/customer/address'
import type { MutationHook } from '@commerce/utils/types'
import { useCallback } from 'react'
import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/customer/address/use-add-item'
import useAddresses from './use-addresses'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
url: '/api/customer/address',
method: 'POST',
},
async fetcher({ input: item, options, 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({
...options,
body: { item },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
const { mutate } = useAddresses()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,33 @@
import type { GetAddressesHook } from '@commerce/types/customer/address'
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useAddresses, { UseAddresses } from '@commerce/customer/address/use-addresses'
export default useAddresses as UseAddresses<typeof handler>
export const handler: SWRHook<GetAddressesHook> = {
fetchOptions: {
url: '/api/customer/address',
method: 'GET',
},
useHook: ({ useData }) =>
function useHook(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]
)
},
}

View File

@ -0,0 +1,60 @@
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import type { Cart, LineItem, RemoveItemHook } from '@commerce/types/cart'
import { useCallback } from 'react'
import { ValidationError } from '@commerce/utils/errors'
import useRemoveItem, { UseRemoveItem } from '@commerce/customer/address/use-remove-item'
import useAddresses from './use-addresses'
export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
export type RemoveItemActionInput<T = any> = T extends LineItem
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/customer/address',
method: 'DELETE',
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveItemHook>) {
return await fetch({ ...options, body: { itemId } })
},
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) =>
function useHook<T extends LineItem | undefined = undefined>(
ctx: { item?: T } = {}
) {
const { item } = ctx
const { mutate } = useAddresses()
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 fetch({ input: { itemId } })
await mutate(data, false)
return data
}
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
},
}

View File

@ -0,0 +1,93 @@
import type {
HookFetcherContext,
MutationHookContext,
} from '@commerce/utils/types'
import type { UpdateItemHook, LineItem } from '@commerce/types/cart'
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import { MutationHook } from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useUpdateItem, { UseUpdateItem } from '@commerce/customer/address/use-update-item'
import { handler as removeItemHandler } from './use-remove-item'
import useAddresses from './use-addresses'
export type UpdateItemActionInput<T = any> = T extends LineItem
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = {
fetchOptions: {
url: '/api/customer/address',
method: 'PUT',
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateItemHook>) {
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 removeItemHandler.fetcher({
options: removeItemHandler.fetchOptions,
input: { itemId },
fetch,
})
}
} else if (item.quantity) {
throw new ValidationError({
message: 'The item quantity has to be a valid integer',
})
}
return await fetch({
...options,
body: { itemId, item },
})
},
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) =>
function useHook<T extends LineItem | undefined = undefined>(
ctx: {
item?: T
wait?: number
} = {}
) {
const { item } = ctx
const { mutate } = useAddresses() as any
return useCallback(
debounce(async (input: UpdateItemActionInput<T>) => {
const itemId = input.id ?? item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId
if (!itemId || !productId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({
input: {
itemId,
item: {
productId,
variantId: variantId || '',
quantity: input.quantity,
},
},
})
await mutate(data, false)
return data
}, ctx.wait ?? 500),
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,4 @@
export { default as useCards } from './use-cards'
export { default as useAddItem } from './use-add-item'
export { default as useRemoveItem } from './use-remove-item'
export { default as useUpdateItem } from './use-update-item'

View File

@ -0,0 +1,48 @@
import type { AddItemHook } from '@commerce/types/customer/card'
import type { MutationHook } from '@commerce/utils/types'
import { useCallback } from 'react'
import { CommerceError } from '@commerce/utils/errors'
import useAddItem, { UseAddItem } from '@commerce/customer/card/use-add-item'
import useCards from './use-cards'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<AddItemHook> = {
fetchOptions: {
url: '/api/customer/card',
method: 'POST',
},
async fetcher({ input: item, options, 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({
...options,
body: { item },
})
return data
},
useHook: ({ fetch }) =>
function useHook() {
const { mutate } = useCards()
return useCallback(
async function addItem(input) {
const data = await fetch({ input })
await mutate(data, false)
return data
},
[fetch, mutate]
)
},
}

View File

@ -0,0 +1,33 @@
import type { GetCardsHook } from '@commerce/types/customer/card'
import { useMemo } from 'react'
import { SWRHook } from '@commerce/utils/types'
import useCard, { UseCards } from '@commerce/customer/card/use-cards'
export default useCard as UseCards<typeof handler>
export const handler: SWRHook<GetCardsHook> = {
fetchOptions: {
url: '/api/customer/card',
method: 'GET',
},
useHook: ({ useData }) =>
function useHook(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]
)
},
}

View File

@ -0,0 +1,60 @@
import type {
MutationHookContext,
HookFetcherContext,
} from '@commerce/utils/types'
import type { Cart, LineItem, RemoveItemHook } from '@commerce/types/customer/card'
import { useCallback } from 'react'
import { ValidationError } from '@commerce/utils/errors'
import useRemoveItem, { UseRemoveItem } from '@commerce/customer/card/use-remove-item'
import useCards from './use-cards'
export type RemoveItemFn<T = any> = T extends LineItem
? (input?: RemoveItemActionInput<T>) => Promise<Cart | null | undefined>
: (input: RemoveItemActionInput<T>) => Promise<Cart | null>
export type RemoveItemActionInput<T = any> = T extends LineItem
? Partial<RemoveItemHook['actionInput']>
: RemoveItemHook['actionInput']
export default useRemoveItem as UseRemoveItem<typeof handler>
export const handler = {
fetchOptions: {
url: '/api/customer/card',
method: 'DELETE',
},
async fetcher({
input: { itemId },
options,
fetch,
}: HookFetcherContext<RemoveItemHook>) {
return await fetch({ ...options, body: { itemId } })
},
useHook: ({ fetch }: MutationHookContext<RemoveItemHook>) =>
function useHook<T extends LineItem | undefined = undefined>(
ctx: { item?: T } = {}
) {
const { item } = ctx
const { mutate } = useCards()
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 fetch({ input: { itemId } })
await mutate(data, false)
return data
}
return useCallback(removeItem as RemoveItemFn<T>, [fetch, mutate])
},
}

View File

@ -0,0 +1,93 @@
import type {
HookFetcherContext,
MutationHookContext,
} from '@commerce/utils/types'
import type { UpdateItemHook, LineItem } from '@commerce/types/customer/card'
import { useCallback } from 'react'
import debounce from 'lodash.debounce'
import { MutationHook } from '@commerce/utils/types'
import { ValidationError } from '@commerce/utils/errors'
import useUpdateItem, { UseUpdateItem } from '@commerce/customer/card/use-update-item'
import { handler as removeItemHandler } from './use-remove-item'
import useCards from './use-cards'
export type UpdateItemActionInput<T = any> = T extends LineItem
? Partial<UpdateItemHook['actionInput']>
: UpdateItemHook['actionInput']
export default useUpdateItem as UseUpdateItem<any>
export const handler: MutationHook<any> = {
fetchOptions: {
url: '/api/customer/card',
method: 'PUT',
},
async fetcher({
input: { itemId, item },
options,
fetch,
}: HookFetcherContext<UpdateItemHook>) {
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 removeItemHandler.fetcher({
options: removeItemHandler.fetchOptions,
input: { itemId },
fetch,
})
}
} else if (item.quantity) {
throw new ValidationError({
message: 'The item quantity has to be a valid integer',
})
}
return await fetch({
...options,
body: { itemId, item },
})
},
useHook: ({ fetch }: MutationHookContext<UpdateItemHook>) =>
function useHook<T extends LineItem | undefined = undefined>(
ctx: {
item?: T
wait?: number
} = {}
) {
const { item } = ctx
const { mutate } = useCards() as any
return useCallback(
debounce(async (input: UpdateItemActionInput<T>) => {
const itemId = input.id ?? item?.id
const productId = input.productId ?? item?.productId
const variantId = input.productId ?? item?.variantId
if (!itemId || !productId) {
throw new ValidationError({
message: 'Invalid input used for this operation',
})
}
const data = await fetch({
input: {
itemId,
item: {
productId,
variantId: variantId || '',
quantity: input.quantity,
},
},
})
await mutate(data, false)
return data
}, ctx.wait ?? 500),
[fetch, mutate]
)
},
}

View File

@ -1,7 +1,7 @@
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 useAddCartItem } from './cart/use-add-item'
import { handler as useUpdateCartItem } from './cart/use-update-item'
import { handler as useRemoveCartItem } from './cart/use-remove-item'
import { handler as useCustomer } from './customer/use-customer'
import { handler as useSearch } from './product/use-search'
@ -10,6 +10,20 @@ 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 { handler as useCheckout } from './checkout/use-checkout'
import { handler as useSubmitCheckout } from './checkout/use-submit-checkout'
import { handler as useGetCheckout } from './checkout/use-get-checkout'
import { handler as useCards } from './customer/card/use-cards'
import { handler as useAddCardItem } from './customer/card/use-add-item'
import { handler as useUpdateCardItem } from './customer/card/use-update-item'
import { handler as useRemoveCardItem } from './customer/card/use-remove-item'
import { handler as useAddresses } from './customer/address/use-addresses'
import { handler as useAddAddressItem } from './customer/address/use-add-item'
import { handler as useUpdateAddressItem } from './customer/address/use-update-item'
import { handler as useRemoveAddressItem } from './customer/address/use-remove-item'
import { CART_COOKIE, LOCALE } from './constants'
import { default as fetcher } from './fetcher'
@ -17,8 +31,32 @@ export const ordercloudProvider = {
locale: LOCALE,
cartCookie: CART_COOKIE,
fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: { useCustomer },
cart: {
useCart,
useAddItem: useAddCartItem,
useUpdateItem: useUpdateCartItem,
useRemoveItem: useRemoveCartItem
},
checkout: {
useCheckout,
useSubmitCheckout,
useGetCheckout,
},
customer: {
useCustomer,
card: {
useCards,
useAddItem: useAddCardItem,
useUpdateItem: useUpdateCardItem,
useRemoveItem: useRemoveCardItem
},
address: {
useAddresses,
useAddItem: useAddAddressItem,
useUpdateItem: useUpdateAddressItem,
useRemoveItem: useRemoveAddressItem
}
},
products: { useSearch },
auth: { useLogin, useLogout, useSignup },
}

View File

@ -0,0 +1,4 @@
import * as Core from '@commerce/types/checkout'
export type CheckoutTypes = Core.CheckoutTypes
export type CheckoutSchema = Core.CheckoutSchema<CheckoutTypes>

View File

@ -0,0 +1,31 @@
import * as Core from '@commerce/types/customer/address'
export type CustomerAddressTypes = Core.CustomerAddressTypes
export type CustomerAddressSchema = Core.CustomerAddressSchema<CustomerAddressTypes>
export interface OrdercloudAddress {
ID: string;
"FromCompanyID": string;
"ToCompanyID": string;
"FromUserID": string;
"BillingAddressID": null,
"BillingAddress": null,
"ShippingAddressID": null,
"Comments": null,
"LineItemCount": number;
"Status": string;
"DateCreated": string;
"DateSubmitted": null,
"DateApproved": null,
"DateDeclined": null,
"DateCanceled": null,
"DateCompleted": null,
"LastUpdated": string;
"Subtotal": number
"ShippingCost": number
"TaxCost": number
"PromotionDiscount": number
"Total": number
"IsSubmitted": false,
"xp": null
}

View File

@ -0,0 +1,16 @@
import * as Core from '@commerce/types/customer/card'
export type CustomerCardTypes = Core.CustomerCardTypes
export type CustomerCardSchema = Core.CustomerCardSchema<CustomerCardTypes>
export interface OredercloudCreditCard {
"ID": string;
"Editable": boolean;
"Token": string;
"DateCreated": string;
"CardType": string;
"PartialAccountNumber": string;
"CardholderName": string;
"ExpirationDate": string;
"xp": null
}

View File

@ -43,6 +43,7 @@
"react-fast-marquee": "^1.1.4",
"react-merge-refs": "^1.1.0",
"react-use-measure": "^2.0.4",
"stripe": "^8.174.0",
"swell-js": "^4.0.0-next.0",
"swr": "^0.5.6",
"tabbable": "^5.2.0",

View File

@ -0,0 +1,4 @@
import customerAddressApi from '@framework/api/endpoints/customer/address'
import commerce from '@lib/api/commerce'
export default customerAddressApi(commerce)

View File

@ -0,0 +1,4 @@
import customerCardApi from '@framework/api/endpoints/customer/card'
import commerce from '@lib/api/commerce'
export default customerCardApi(commerce)

View File

@ -23,8 +23,8 @@
"@components/*": ["components/*"],
"@commerce": ["framework/commerce"],
"@commerce/*": ["framework/commerce/*"],
"@framework": ["framework/local"],
"@framework/*": ["framework/local/*"]
"@framework": ["framework/ordercloud"],
"@framework/*": ["framework/ordercloud/*"]
}
},
"include": ["next-env.d.ts", "**/*.d.ts", "**/*.ts", "**/*.tsx", "**/*.js"],

View File

@ -1170,6 +1170,11 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67"
integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ==
"@types/node@>=8.1.0":
version "16.9.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.9.1.tgz#0611b37db4246c937feef529ddcc018cf8e35708"
integrity sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g==
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -5686,6 +5691,13 @@ qs@6.7.0:
resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc"
integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
qs@^6.6.0:
version "6.10.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.1.tgz#4931482fa8d647a5aab799c5271d2133b981fb6a"
integrity sha512-M528Hph6wsSVOBiYUnGf+K/7w0hNshs/duGsNXPUCLH5XAqjEtiPGwNONLV0tBH8NoGb0mvD5JubnUTrujKDTg==
dependencies:
side-channel "^1.0.4"
querystring-es3@0.2.1, querystring-es3@^0.2.0:
version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
@ -6487,6 +6499,14 @@ strip-json-comments@~2.0.1:
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a"
integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo=
stripe@^8.174.0:
version "8.174.0"
resolved "https://registry.yarnpkg.com/stripe/-/stripe-8.174.0.tgz#91d2e61b0217b1ee9fde2842582e0f1cf1dddc94"
integrity sha512-UFU5TuYH7XwUmSllUIcIKhhsvvhhjw9D6ZwVdfB74wU4VOOaWBiQqszkw6chaEFpdulUmbcAH5eZltV3HwOi7g==
dependencies:
"@types/node" ">=8.1.0"
qs "^6.6.0"
styled-jsx@3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.2.tgz#2474601a26670a6049fb4d3f94bd91695b3ce018"