Merge branch 'main' of https://github.com/vercel/commerce into elasticpath-master

This commit is contained in:
GunaTrika 2021-10-06 11:50:01 +05:30
commit 19300cbafc
238 changed files with 5349 additions and 12427 deletions

View File

@ -1,4 +1,4 @@
# Available providers: bigcommerce, shopify, swell # Available providers: local, bigcommerce, shopify, swell, saleor
COMMERCE_PROVIDER= COMMERCE_PROVIDER=
BIGCOMMERCE_STOREFRONT_API_URL= BIGCOMMERCE_STOREFRONT_API_URL=
@ -7,6 +7,10 @@ BIGCOMMERCE_STORE_API_URL=
BIGCOMMERCE_STORE_API_TOKEN= BIGCOMMERCE_STORE_API_TOKEN=
BIGCOMMERCE_STORE_API_CLIENT_ID= BIGCOMMERCE_STORE_API_CLIENT_ID=
BIGCOMMERCE_CHANNEL_ID= BIGCOMMERCE_CHANNEL_ID=
BIGCOMMERCE_STORE_URL=
BIGCOMMERCE_STORE_API_STORE_HASH=
BIGCOMMERCE_STORE_API_CLIENT_SECRET=
NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN= NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN=
NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN= NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN=
@ -16,3 +20,10 @@ NEXT_PUBLIC_SWELL_PUBLIC_KEY=
NEXT_PUBLIC_SALEOR_API_URL= NEXT_PUBLIC_SALEOR_API_URL=
NEXT_PUBLIC_SALEOR_CHANNEL= NEXT_PUBLIC_SALEOR_CHANNEL=
NEXT_PUBLIC_VENDURE_SHOP_API_URL=
NEXT_PUBLIC_VENDURE_LOCAL_URL=
ORDERCLOUD_CLIENT_ID=
ORDERCLOUD_CLIENT_SECRET=
STRIPE_SECRET=

6
.eslintrc Normal file
View File

@ -0,0 +1,6 @@
{
"extends": ["next", "prettier"],
"rules": {
"react/no-unescaped-entities": "off"
}
}

View File

@ -66,7 +66,13 @@ Every provider defines the features that it supports under `framework/{provider}
#### Features Available #### Features Available
The following features can be enabled or disabled. This means that the UI will remove all code related to the feature.
For example: Turning `cart` off will disable Cart capabilities.
- cart
- search
- wishlist - wishlist
- customerAuth
- customCheckout - customCheckout
#### How to turn Features on and off #### How to turn Features on and off
@ -83,7 +89,7 @@ Every provider defines the features that it supports under `framework/{provider}
} }
} }
``` ```
- Turn wishlist on by setting wishlist to true. - Turn `wishlist` on by setting `wishlist` to `true`.
- Run the app and the wishlist functionality should be back on. - Run the app and the wishlist functionality should be back on.
### How to create a new provider ### How to create a new provider
@ -145,5 +151,5 @@ Next, you're free to customize the starter. More updates coming soon. Stay tuned
After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard. After Email confirmation, Checkout should be manually enabled through BigCommerce platform. Look for "Review & test your store" section through BigCommerce's dashboard.
<br> <br>
<br> <br>
BigCommerce team has been notified and they plan to add more detailed about this subject. BigCommerce team has been notified and they plan to add more details about this subject.
</details> </details>

View File

@ -77,7 +77,6 @@ html {
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
touch-action: manipulation; touch-action: manipulation;
font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;

View File

@ -38,6 +38,7 @@ const LoginView: FC<Props> = () => {
} catch ({ errors }) { } catch ({ errors }) {
setMessage(errors[0].message) setMessage(errors[0].message)
setLoading(false) setLoading(false)
setDisabled(false)
} }
} }

View File

@ -70,6 +70,9 @@ const CartItem = ({
if (item.quantity !== Number(quantity)) { if (item.quantity !== Number(quantity)) {
setQuantity(item.quantity) setQuantity(item.quantity)
} }
// TODO: currently not including quantity in deps is intended, but we should
// do this differently as it could break easily
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [item.quantity]) }, [item.quantity])
return ( return (
@ -82,25 +85,29 @@ const CartItem = ({
<div className="flex flex-row space-x-4 py-4"> <div className="flex flex-row space-x-4 py-4">
<div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer z-0"> <div className="w-16 h-16 bg-violet relative overflow-hidden cursor-pointer z-0">
<Link href={`/product/${item.path}`}> <Link href={`/product/${item.path}`}>
<Image <a>
onClick={() => closeSidebarIfPresent()} <Image
className={s.productImage} onClick={() => closeSidebarIfPresent()}
width={150} className={s.productImage}
height={150} width={150}
src={item.variant.image!.url} height={150}
alt={item.variant.image!.altText} src={item.variant.image!.url}
unoptimized alt={item.variant.image!.altText}
/> unoptimized
/>
</a>
</Link> </Link>
</div> </div>
<div className="flex-1 flex flex-col text-base"> <div className="flex-1 flex flex-col text-base">
<Link href={`/product/${item.path}`}> <Link href={`/product/${item.path}`}>
<span <a>
className={s.productName} <span
onClick={() => closeSidebarIfPresent()} className={s.productName}
> onClick={() => closeSidebarIfPresent()}
{item.name} >
</span> {item.name}
</span>
</a>
</Link> </Link>
{options && options.length > 0 && ( {options && options.length > 0 && (
<div className="flex items-center pb-1"> <div className="flex items-center pb-1">

View File

@ -74,9 +74,11 @@ const CartSidebarView: FC = () => {
<> <>
<div className="px-4 sm:px-6 flex-1"> <div className="px-4 sm:px-6 flex-1">
<Link href="/cart"> <Link href="/cart">
<Text variant="sectionHeading" onClick={handleClose}> <a>
My Cart <Text variant="sectionHeading" onClick={handleClose}>
</Text> My Cart
</Text>
</a>
</Link> </Link>
<ul className={s.lineItemsList}> <ul className={s.lineItemsList}>
{data!.lineItems.map((item: any) => ( {data!.lineItems.map((item: any) => (

View File

@ -1,30 +1,39 @@
import cn from 'classnames'
import Link from 'next/link' import Link from 'next/link'
import { FC } from 'react' import { FC } from 'react'
import CartItem from '@components/cart/CartItem' import CartItem from '@components/cart/CartItem'
import { Button, Text } from '@components/ui' import { Button, Text } from '@components/ui'
import { useUI } from '@components/ui/context' import { useUI } from '@components/ui/context'
import SidebarLayout from '@components/common/SidebarLayout'
import useCart from '@framework/cart/use-cart' import useCart from '@framework/cart/use-cart'
import usePrice from '@framework/product/use-price' import usePrice from '@framework/product/use-price'
import useCheckout from '@framework/checkout/use-checkout'
import ShippingWidget from '../ShippingWidget' import ShippingWidget from '../ShippingWidget'
import PaymentWidget from '../PaymentWidget' import PaymentWidget from '../PaymentWidget'
import SidebarLayout from '@components/common/SidebarLayout'
import s from './CheckoutSidebarView.module.css' import s from './CheckoutSidebarView.module.css'
const CheckoutSidebarView: FC = () => { const CheckoutSidebarView: FC = () => {
const { setSidebarView } = useUI() const { setSidebarView, closeSidebar } = useUI()
const { data } = useCart() 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( const { price: subTotal } = usePrice(
data && { cartData && {
amount: Number(data.subtotalPrice), amount: Number(cartData.subtotalPrice),
currencyCode: data.currency.code, currencyCode: cartData.currency.code,
} }
) )
const { price: total } = usePrice( const { price: total } = usePrice(
data && { cartData && {
amount: Number(data.totalPrice), amount: Number(cartData.totalPrice),
currencyCode: data.currency.code, currencyCode: cartData.currency.code,
} }
) )
@ -35,25 +44,36 @@ const CheckoutSidebarView: FC = () => {
> >
<div className="px-4 sm:px-6 flex-1"> <div className="px-4 sm:px-6 flex-1">
<Link href="/cart"> <Link href="/cart">
<Text variant="sectionHeading">Checkout</Text> <a>
<Text variant="sectionHeading">Checkout</Text>
</a>
</Link> </Link>
<PaymentWidget onClick={() => setSidebarView('PAYMENT_VIEW')} /> <PaymentWidget
<ShippingWidget onClick={() => setSidebarView('SHIPPING_VIEW')} /> isValid={checkoutData?.hasPayment}
onClick={() => setSidebarView('PAYMENT_VIEW')}
/>
<ShippingWidget
isValid={checkoutData?.hasShipping}
onClick={() => setSidebarView('SHIPPING_VIEW')}
/>
<ul className={s.lineItemsList}> <ul className={s.lineItemsList}>
{data!.lineItems.map((item: any) => ( {cartData!.lineItems.map((item: any) => (
<CartItem <CartItem
key={item.id} key={item.id}
item={item} item={item}
currencyCode={data!.currency.code} currencyCode={cartData!.currency.code}
variant="display" variant="display"
/> />
))} ))}
</ul> </ul>
</div> </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"> <ul className="pb-2">
<li className="flex justify-between py-1"> <li className="flex justify-between py-1">
<span>Subtotal</span> <span>Subtotal</span>
@ -74,14 +94,15 @@ const CheckoutSidebarView: FC = () => {
</div> </div>
<div> <div>
{/* Once data is correcly filled */} {/* Once data is correcly filled */}
{/* <Button Component="a" width="100%"> <Button
Confirm Purchase type="submit"
</Button> */} width="100%"
<Button Component="a" width="100%" variant="ghost" disabled> disabled={!checkoutData?.hasPayment || !checkoutData?.hasShipping}
Continue >
Confirm Purchase
</Button> </Button>
</div> </div>
</div> </form>
</SidebarLayout> </SidebarLayout>
) )
} }

View File

@ -1,83 +1,129 @@
import { FC } from 'react' import { FC } from 'react'
import cn from 'classnames' import cn from 'classnames'
import useAddCard from '@framework/customer/card/use-add-item'
import { Button, Text } from '@components/ui' import { Button, Text } from '@components/ui'
import { useUI } from '@components/ui/context' import { useUI } from '@components/ui/context'
import s from './PaymentMethodView.module.css'
import SidebarLayout from '@components/common/SidebarLayout' 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 PaymentMethodView: FC = () => {
const { setSidebarView } = useUI() const { setSidebarView } = useUI()
const addCard = useAddCard()
async function handleSubmit(event: React.ChangeEvent<Form>) {
event.preventDefault()
await addCard({
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 ( return (
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}> <form className="h-full" onSubmit={handleSubmit}>
<div className="px-4 sm:px-6 flex-1"> <SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
<Text variant="sectionHeading"> Payment Method</Text> <div className="px-4 sm:px-6 flex-1">
<div> <Text variant="sectionHeading"> Payment Method</Text>
<div className={s.fieldset}> <div>
<label className={s.label}>Cardholder Name</label> <div className={s.fieldset}>
<input className={s.input} /> <label className={s.label}>Cardholder Name</label>
</div> <input name="cardHolder" className={s.input} />
<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} />
</div> </div>
<div className={cn(s.fieldset, 'col-span-3')}> <div className="grid gap-3 grid-flow-row grid-cols-12">
<label className={s.label}>Expires</label> <div className={cn(s.fieldset, 'col-span-7')}>
<input className={s.input} placeholder="MM/YY" /> <label className={s.label}>Card Number</label>
<input name="cardNumber" className={s.input} />
</div>
<div className={cn(s.fieldset, 'col-span-3')}>
<label className={s.label}>Expires</label>
<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 name="cardCvc" className={s.input} />
</div>
</div> </div>
<div className={cn(s.fieldset, 'col-span-2')}> <hr className="border-accent-2 my-6" />
<label className={s.label}>CVC</label> <div className="grid gap-3 grid-flow-row grid-cols-12">
<input className={s.input} /> <div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>First Name</label>
<input name="firstName" className={s.input} />
</div>
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>Last Name</label>
<input name="lastName" className={s.input} />
</div>
</div> </div>
</div> <div className={s.fieldset}>
<hr className="border-accent-2 my-6" /> <label className={s.label}>Company (Optional)</label>
<div className="grid gap-3 grid-flow-row grid-cols-12"> <input name="company" className={s.input} />
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>First Name</label>
<input className={s.input} />
</div> </div>
<div className={cn(s.fieldset, 'col-span-6')}> <div className={s.fieldset}>
<label className={s.label}>Last Name</label> <label className={s.label}>Street and House Number</label>
<input className={s.input} /> <input name="streetNumber" className={s.input} />
</div> </div>
</div> <div className={s.fieldset}>
<div className={s.fieldset}> <label className={s.label}>
<label className={s.label}>Company (Optional)</label> Apartment, Suite, Etc. (Optional)
<input className={s.input} /> </label>
</div> <input className={s.input} name="apartment" />
<div className={s.fieldset}>
<label className={s.label}>Street and House Number</label>
<input className={s.input} />
</div>
<div className={s.fieldset}>
<label className={s.label}>Apartment, Suite, Etc. (Optional)</label>
<input 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} />
</div> </div>
<div className={cn(s.fieldset, 'col-span-6')}> <div className="grid gap-3 grid-flow-row grid-cols-12">
<label className={s.label}>City</label> <div className={cn(s.fieldset, 'col-span-6')}>
<input className={s.input} /> <label className={s.label}>Postal Code</label>
<input name="zipCode" className={s.input} />
</div>
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>City</label>
<input name="city" className={s.input} />
</div>
</div>
<div className={s.fieldset}>
<label className={s.label}>Country/Region</label>
<select name="country" className={s.select}>
<option>Hong Kong</option>
</select>
</div> </div>
</div>
<div className={s.fieldset}>
<label className={s.label}>Country/Region</label>
<select className={s.select}>
<option>Hong Kong</option>
</select>
</div> </div>
</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">
<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 type="submit" width="100%" variant="ghost">
<Button Component="a" width="100%" variant="ghost"> Continue
Continue </Button>
</Button> </div>
</div> </SidebarLayout>
</SidebarLayout> </form>
) )
} }

View File

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

View File

@ -1,77 +1,117 @@
import { FC } from 'react' import { FC } from 'react'
import cn from 'classnames' import cn from 'classnames'
import s from './ShippingView.module.css'
import Button from '@components/ui/Button' import Button from '@components/ui/Button'
import { useUI } from '@components/ui/context' import { useUI } from '@components/ui/context'
import SidebarLayout from '@components/common/SidebarLayout' import SidebarLayout from '@components/common/SidebarLayout'
import useAddAddress from '@framework/customer/address/use-add-item'
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 PaymentMethodView: FC = () => {
const { setSidebarView } = useUI() const { setSidebarView } = useUI()
const addAddress = useAddAddress()
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 ( return (
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}> <form className="h-full" onSubmit={handleSubmit}>
<div className="px-4 sm:px-6 flex-1"> <SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
<h2 className="pt-1 pb-8 text-2xl font-semibold tracking-wide cursor-pointer inline-block"> <div className="px-4 sm:px-6 flex-1">
Shipping <h2 className="pt-1 pb-8 text-2xl font-semibold tracking-wide cursor-pointer inline-block">
</h2> Shipping
<div> </h2>
<div className="flex flex-row my-3 items-center"> <div>
<input className={s.radio} type="radio" /> <div className="flex flex-row my-3 items-center">
<span className="ml-3 text-sm">Same as billing address</span> <input name="type" className={s.radio} type="radio" />
</div> <span className="ml-3 text-sm">Same as billing address</span>
<div className="flex flex-row my-3 items-center">
<input className={s.radio} type="radio" />
<span className="ml-3 text-sm">
Use a different shipping address
</span>
</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} />
</div> </div>
<div className={cn(s.fieldset, 'col-span-6')}> <div className="flex flex-row my-3 items-center">
<label className={s.label}>Last Name</label> <input name="type" className={s.radio} type="radio" />
<input className={s.input} /> <span className="ml-3 text-sm">
Use a different shipping address
</span>
</div> </div>
</div> <hr className="border-accent-2 my-6" />
<div className={s.fieldset}> <div className="grid gap-3 grid-flow-row grid-cols-12">
<label className={s.label}>Company (Optional)</label> <div className={cn(s.fieldset, 'col-span-6')}>
<input className={s.input} /> <label className={s.label}>First Name</label>
</div> <input name="firstName" className={s.input} />
<div className={s.fieldset}> </div>
<label className={s.label}>Street and House Number</label> <div className={cn(s.fieldset, 'col-span-6')}>
<input className={s.input} /> <label className={s.label}>Last Name</label>
</div> <input name="lastName" className={s.input} />
<div className={s.fieldset}> </div>
<label className={s.label}>Apartment, Suite, Etc. (Optional)</label>
<input 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} />
</div> </div>
<div className={cn(s.fieldset, 'col-span-6')}> <div className={s.fieldset}>
<label className={s.label}>City</label> <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 name="streetNumber" className={s.input} />
</div>
<div className={s.fieldset}>
<label className={s.label}>
Apartment, Suite, Etc. (Optional)
</label>
<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 name="zipCode" className={s.input} />
</div>
<div className={cn(s.fieldset, 'col-span-6')}>
<label className={s.label}>City</label>
<input name="city" className={s.input} />
</div>
</div>
<div className={s.fieldset}>
<label className={s.label}>Country/Region</label>
<select name="country" className={s.select}>
<option>Hong Kong</option>
</select>
</div> </div>
</div>
<div className={s.fieldset}>
<label className={s.label}>Country/Region</label>
<select className={s.select}>
<option>Hong Kong</option>
</select>
</div> </div>
</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">
<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 type="submit" width="100%" variant="ghost">
<Button Component="a" width="100%" variant="ghost"> Continue
Continue </Button>
</Button> </div>
</div> </SidebarLayout>
</SidebarLayout> </form>
) )
} }

View File

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

View File

@ -64,7 +64,6 @@ const Footer: FC<Props> = ({ className, pages }) => {
<span>&copy; 2020 CKUBE. All rights reserved.</span> <span>&copy; 2020 CKUBE. All rights reserved.</span>
</div> </div>
<div className="flex items-center text-primary text-sm"> <div className="flex items-center text-primary text-sm">
</div> </div>
</div> </div>
</Container> </Container>

View File

@ -24,7 +24,7 @@ const Loading = () => (
) )
const dynamicProps = { const dynamicProps = {
loading: () => <Loading />, loading: Loading,
} }
const SignUpView = dynamic( const SignUpView = dynamic(

View File

@ -1,5 +1,6 @@
.root { .root {
@apply sticky top-0 bg-primary z-40 transition-all duration-150; @apply sticky top-0 bg-primary z-40 transition-all duration-150;
min-height: 74px;
} }
.nav { .nav {

View File

@ -34,9 +34,11 @@ const Navbar: FC<NavbarProps> = ({ links }) => (
))} ))}
</nav> </nav>
</div> </div>
<div className="justify-center flex-1 hidden lg:flex"> {process.env.COMMERCE_SEARCH_ENABLED && (
<Searchbar /> <div className="justify-center flex-1 hidden lg:flex">
</div> <Searchbar />
</div>
)}
<div className="flex items-center justify-end flex-1 space-x-8"> <div className="flex items-center justify-end flex-1 space-x-8">
<UserNav /> <UserNav />
</div> </div>

View File

@ -1,4 +1,4 @@
import { FC, InputHTMLAttributes, useEffect, useMemo } from 'react' import { FC, memo, useEffect } from 'react'
import cn from 'classnames' import cn from 'classnames'
import s from './Searchbar.module.css' import s from './Searchbar.module.css'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
@ -13,7 +13,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
useEffect(() => { useEffect(() => {
router.prefetch('/search') router.prefetch('/search')
}, []) }, [router])
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
e.preventDefault() e.preventDefault()
@ -32,32 +32,29 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
} }
} }
return useMemo( return (
() => ( <div className={cn(s.root, className)}>
<div className={cn(s.root, className)}> <label className="hidden" htmlFor={id}>
<label className="hidden" htmlFor={id}> Search
Search </label>
</label> <input
<input id={id}
id={id} className={s.input}
className={s.input} placeholder="Search for products..."
placeholder="Search for products..." defaultValue={router.query.q}
defaultValue={router.query.q} onKeyUp={handleKeyUp}
onKeyUp={handleKeyUp} />
/> <div className={s.iconContainer}>
<div className={s.iconContainer}> <svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20"> <path
<path fillRule="evenodd"
fillRule="evenodd" clipRule="evenodd"
clipRule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" />
/> </svg>
</svg>
</div>
</div> </div>
), </div>
[]
) )
} }
export default Searchbar export default memo(Searchbar)

View File

@ -7,6 +7,7 @@ import useCustomer from '@framework/customer/use-customer'
import { Avatar } from '@components/common' import { Avatar } from '@components/common'
import { Heart, Bag } from '@components/icons' import { Heart, Bag } from '@components/icons'
import { useUI } from '@components/ui/context' import { useUI } from '@components/ui/context'
import Button from '@components/ui/Button'
import DropdownMenu from './DropdownMenu' import DropdownMenu from './DropdownMenu'
import s from './UserNav.module.css' import s from './UserNav.module.css'
@ -25,10 +26,14 @@ const UserNav: FC<Props> = ({ className }) => {
return ( return (
<nav className={cn(s.root, className)}> <nav className={cn(s.root, className)}>
<ul className={s.list}> <ul className={s.list}>
<li className={s.item} onClick={toggleSidebar}> {process.env.COMMERCE_CART_ENABLED && (
<Bag /> <li className={s.item}>
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>} <Button className={s.item} variant="naked" onClick={toggleSidebar} aria-label="Cart">
</li> <Bag />
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
</Button>
</li>
)}
{process.env.COMMERCE_WISHLIST_ENABLED && ( {process.env.COMMERCE_WISHLIST_ENABLED && (
<li className={s.item}> <li className={s.item}>
<Link href="/wishlist"> <Link href="/wishlist">
@ -38,7 +43,7 @@ const UserNav: FC<Props> = ({ className }) => {
</Link> </Link>
</li> </li>
)} )}
{process.env.COMMERCE_CUSTOMER_ENABLED && ( {process.env.COMMERCE_CUSTOMERAUTH_ENABLED && (
<li className={s.item}> <li className={s.item}>
{customer ? ( {customer ? (
<DropdownMenu /> <DropdownMenu />

View File

@ -7,11 +7,12 @@ import Image, { ImageProps } from 'next/image'
import WishlistButton from '@components/wishlist/WishlistButton' import WishlistButton from '@components/wishlist/WishlistButton'
import usePrice from '@framework/product/use-price' import usePrice from '@framework/product/use-price'
import ProductTag from '../ProductTag' import ProductTag from '../ProductTag'
interface Props { interface Props {
className?: string className?: string
product: Product product: Product
noNameTag?: boolean noNameTag?: boolean
imgProps?: Omit<ImageProps, 'src'> imgProps?: Omit<ImageProps, 'src' | 'layout' | 'placeholder' | 'blurDataURL'>
variant?: 'default' | 'slim' | 'simple' variant?: 'default' | 'slim' | 'simple'
} }
@ -23,7 +24,6 @@ const ProductCard: FC<Props> = ({
className, className,
noNameTag = false, noNameTag = false,
variant = 'default', variant = 'default',
...props
}) => { }) => {
const { price } = usePrice({ const { price } = usePrice({
amount: product.price.value, amount: product.price.value,
@ -38,7 +38,7 @@ const ProductCard: FC<Props> = ({
) )
return ( return (
<Link href={`/product/${product.slug}`} {...props}> <Link href={`/product/${product.slug}`}>
<a className={rootClassName}> <a className={rootClassName}>
{variant === 'slim' && ( {variant === 'slim' && (
<> <>
@ -83,7 +83,7 @@ const ProductCard: FC<Props> = ({
<Image <Image
alt={product.name || 'Product Image'} alt={product.name || 'Product Image'}
className={s.productImage} className={s.productImage}
src={product.images[0].url || placeholderImg} src={product.images[0]?.url || placeholderImg}
height={540} height={540}
width={540} width={540}
quality="85" quality="85"

View File

@ -1,50 +1,52 @@
import { memo } from 'react'
import { Swatch } from '@components/product' import { Swatch } from '@components/product'
import type { ProductOption } from '@commerce/types/product' import type { ProductOption } from '@commerce/types/product'
import { SelectedOptions } from '../helpers' import { SelectedOptions } from '../helpers'
import React from 'react'
interface ProductOptionsProps { interface ProductOptionsProps {
options: ProductOption[] options: ProductOption[]
selectedOptions: SelectedOptions selectedOptions: SelectedOptions
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>> setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
} }
const ProductOptions: React.FC<ProductOptionsProps> = React.memo( const ProductOptions: React.FC<ProductOptionsProps> = ({
({ options, selectedOptions, setSelectedOptions }) => { options,
return ( selectedOptions,
<div> setSelectedOptions,
{options.map((opt) => ( }) => {
<div className="pb-4" key={opt.displayName}> return (
<h2 className="uppercase font-medium text-sm tracking-wide"> <div>
{opt.displayName} {options.map((opt) => (
</h2> <div className="pb-4" key={opt.displayName}>
<div className="flex flex-row py-4"> <h2 className="uppercase font-medium text-sm tracking-wide">
{opt.values.map((v, i: number) => { {opt.displayName}
const active = selectedOptions[opt.displayName.toLowerCase()] </h2>
return ( <div className="flex flex-row py-4">
<Swatch {opt.values.map((v, i: number) => {
key={`${opt.id}-${i}`} const active = selectedOptions[opt.displayName.toLowerCase()]
active={v.label.toLowerCase() === active} return (
variant={opt.displayName} <Swatch
color={v.hexColors ? v.hexColors[0] : ''} key={`${opt.id}-${i}`}
label={v.label} active={v.label.toLowerCase() === active}
onClick={() => { variant={opt.displayName}
setSelectedOptions((selectedOptions) => { color={v.hexColors ? v.hexColors[0] : ''}
return { label={v.label}
...selectedOptions, onClick={() => {
[opt.displayName.toLowerCase()]: setSelectedOptions((selectedOptions) => {
v.label.toLowerCase(), return {
} ...selectedOptions,
}) [opt.displayName.toLowerCase()]: v.label.toLowerCase(),
}} }
/> })
) }}
})} />
</div> )
})}
</div> </div>
))} </div>
</div> ))}
) </div>
} )
) }
export default ProductOptions export default memo(ProductOptions)

View File

@ -23,7 +23,7 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
useEffect(() => { useEffect(() => {
selectDefaultOptionFromProduct(product, setSelectedOptions) selectDefaultOptionFromProduct(product, setSelectedOptions)
}, []) }, [product])
const variant = getProductVariant(product, selectedOptions) const variant = getProductVariant(product, selectedOptions)
const addToCart = async () => { const addToCart = async () => {
@ -56,18 +56,20 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
<div className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div> <div className="text-accent-6 pr-1 font-medium text-sm">36 reviews</div>
</div> </div>
<div> <div>
<Button {process.env.COMMERCE_CART_ENABLED && (
aria-label="Add to Cart" <Button
type="button" aria-label="Add to Cart"
className={s.button} type="button"
onClick={addToCart} className={s.button}
loading={loading} onClick={addToCart}
disabled={variant?.availableForSale === false} loading={loading}
> disabled={variant?.availableForSale === false}
{variant?.availableForSale === false >
? 'Not Available' {variant?.availableForSale === false
: 'Add To Cart'} ? 'Not Available'
</Button> : 'Add To Cart'}
</Button>
)}
</div> </div>
<div className="mt-6"> <div className="mt-6">
<Collapse title="Care"> <Collapse title="Care">

View File

@ -66,17 +66,13 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
event.preventDefault() event.preventDefault()
} }
sliderContainerRef.current!.addEventListener( const slider = sliderContainerRef.current!
'touchstart',
preventNavigation slider.addEventListener('touchstart', preventNavigation)
)
return () => { return () => {
if (sliderContainerRef.current) { if (slider) {
sliderContainerRef.current!.removeEventListener( slider.removeEventListener('touchstart', preventNavigation)
'touchstart',
preventNavigation
)
} }
} }
}, []) }, [])

View File

@ -1,31 +1,30 @@
import { FC, MouseEventHandler, memo } from 'react'
import cn from 'classnames' import cn from 'classnames'
import React from 'react'
import s from './ProductSliderControl.module.css' import s from './ProductSliderControl.module.css'
import { ArrowLeft, ArrowRight } from '@components/icons' import { ArrowLeft, ArrowRight } from '@components/icons'
interface ProductSliderControl { interface ProductSliderControl {
onPrev: React.MouseEventHandler<HTMLButtonElement> onPrev: MouseEventHandler<HTMLButtonElement>
onNext: React.MouseEventHandler<HTMLButtonElement> onNext: MouseEventHandler<HTMLButtonElement>
} }
const ProductSliderControl: React.FC<ProductSliderControl> = React.memo( const ProductSliderControl: FC<ProductSliderControl> = ({ onPrev, onNext }) => (
({ onPrev, onNext }) => ( <div className={s.control}>
<div className={s.control}> <button
<button className={cn(s.leftControl)}
className={cn(s.leftControl)} onClick={onPrev}
onClick={onPrev} aria-label="Previous Product Image"
aria-label="Previous Product Image" >
> <ArrowLeft />
<ArrowLeft /> </button>
</button> <button
<button className={cn(s.rightControl)}
className={cn(s.rightControl)} onClick={onNext}
onClick={onNext} aria-label="Next Product Image"
aria-label="Next Product Image" >
> <ArrowRight />
<ArrowRight /> </button>
</button> </div>
</div>
)
) )
export default ProductSliderControl
export default memo(ProductSliderControl)

View File

@ -58,7 +58,7 @@ const ProductView: FC<ProductViewProps> = ({ product, relatedProducts }) => {
)} )}
</div> </div>
<ProductSidebar product={product} className={s.sidebar} /> <ProductSidebar key={product.id} product={product} className={s.sidebar} />
</div> </div>
<hr className="mt-7 border-accent-2" /> <hr className="mt-7 border-accent-2" />
<section className="py-12 px-6 mb-10"> <section className="py-12 px-6 mb-10">

View File

@ -7,19 +7,19 @@ import { useRouter } from 'next/router'
import { Layout } from '@components/common' import { Layout } from '@components/common'
import { ProductCard } from '@components/product' import { ProductCard } from '@components/product'
import type { Product } from '@commerce/types/product' import type { Product } from '@commerce/types/product'
import { Container, Grid, Skeleton } from '@components/ui' import { Container, Skeleton } from '@components/ui'
import useSearch from '@framework/product/use-search' import useSearch from '@framework/product/use-search'
import getSlug from '@lib/get-slug' import getSlug from '@lib/get-slug'
import rangeMap from '@lib/range-map' import rangeMap from '@lib/range-map'
const SORT = Object.entries({ const SORT = {
'trending-desc': 'Trending', 'trending-desc': 'Trending',
'latest-desc': 'Latest arrivals', 'latest-desc': 'Latest arrivals',
'price-asc': 'Price: Low to high', 'price-asc': 'Price: Low to high',
'price-desc': 'Price: High to low', 'price-desc': 'Price: High to low',
}) }
import { import {
filterQuery, filterQuery,
@ -351,7 +351,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
aria-haspopup="true" aria-haspopup="true"
aria-expanded="true" aria-expanded="true"
> >
{sort ? `Sort: ${sort}` : 'Relevance'} {sort ? SORT[sort as keyof typeof SORT] : 'Relevance'}
<svg <svg
className="-mr-1 ml-2 h-5 w-5" className="-mr-1 ml-2 h-5 w-5"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -398,7 +398,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
</a> </a>
</Link> </Link>
</li> </li>
{SORT.map(([key, text]) => ( {Object.entries(SORT).map(([key, text]) => (
<li <li
key={key} key={key}
className={cn( className={cn(

View File

@ -35,6 +35,15 @@
@apply border-accent-9 bg-accent-9 text-accent-0; @apply border-accent-9 bg-accent-9 text-accent-0;
} }
.naked {
@apply bg-transparent font-semibold border-none shadow-none outline-none py-0 px-0;
}
.naked:hover,
.naked:focus {
@apply bg-transparent border-none;
}
.disabled, .disabled,
.disabled:hover { .disabled:hover {
@apply text-accent-4 border-accent-2 bg-accent-1 cursor-not-allowed; @apply text-accent-4 border-accent-2 bg-accent-1 cursor-not-allowed;

View File

@ -12,7 +12,7 @@ import { LoadingDots } from '@components/ui'
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
href?: string href?: string
className?: string className?: string
variant?: 'flat' | 'slim' | 'ghost' variant?: 'flat' | 'slim' | 'ghost' | 'naked'
active?: boolean active?: boolean
type?: 'submit' | 'reset' | 'button' type?: 'submit' | 'reset' | 'button'
Component?: string | JSXElementConstructor<any> Component?: string | JSXElementConstructor<any>
@ -41,6 +41,7 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
{ {
[s.ghost]: variant === 'ghost', [s.ghost]: variant === 'ghost',
[s.slim]: variant === 'slim', [s.slim]: variant === 'slim',
[s.naked]: variant === 'naked',
[s.loading]: loading, [s.loading]: loading,
[s.disabled]: disabled, [s.disabled]: disabled,
}, },

View File

@ -27,13 +27,15 @@ const Modal: FC<ModalProps> = ({ children, onClose }) => {
) )
useEffect(() => { useEffect(() => {
if (ref.current) { const modal = ref.current
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
if (modal) {
disableBodyScroll(modal, { reserveScrollBarGap: true })
window.addEventListener('keydown', handleKey) window.addEventListener('keydown', handleKey)
} }
return () => { return () => {
if (ref && ref.current) { if (modal) {
enableBodyScroll(ref.current) enableBodyScroll(modal)
} }
clearAllBodyScrollLocks() clearAllBodyScrollLocks()
window.removeEventListener('keydown', handleKey) window.removeEventListener('keydown', handleKey)

View File

@ -1,4 +1,4 @@
import React, { FC } from 'react' import { FC, memo } from 'react'
import rangeMap from '@lib/range-map' import rangeMap from '@lib/range-map'
import { Star } from '@components/icons' import { Star } from '@components/icons'
import cn from 'classnames' import cn from 'classnames'
@ -7,21 +7,19 @@ export interface RatingProps {
value: number value: number
} }
const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => { const Quantity: FC<RatingProps> = ({ value = 5 }) => (
return ( <div className="flex flex-row py-6 text-accent-9">
<div className="flex flex-row py-6 text-accent-9"> {rangeMap(5, (i) => (
{rangeMap(5, (i) => ( <span
<span key={`star_${i}`}
key={`star_${i}`} className={cn('inline-block ml-1 ', {
className={cn('inline-block ml-1 ', { 'text-accent-5': i >= Math.floor(value),
'text-accent-5': i >= Math.floor(value), })}
})} >
> <Star />
<Star /> </span>
</span> ))}
))} </div>
</div> )
)
})
export default Quantity export default memo(Quantity)

View File

@ -1,5 +1,5 @@
.root { .root {
@apply fixed inset-0 h-full z-50 box-border; @apply fixed inset-0 h-full z-50 box-border outline-none;
} }
.sidebar { .sidebar {

View File

@ -13,27 +13,44 @@ interface SidebarProps {
} }
const Sidebar: FC<SidebarProps> = ({ children, onClose }) => { const Sidebar: FC<SidebarProps> = ({ children, onClose }) => {
const ref = useRef() as React.MutableRefObject<HTMLDivElement> const sidebarRef = useRef() as React.MutableRefObject<HTMLDivElement>
const contentRef = useRef() as React.MutableRefObject<HTMLDivElement>
const onKeyDownSidebar = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.code === 'Escape') {
onClose()
}
}
useEffect(() => { useEffect(() => {
if (ref.current) { if (sidebarRef.current) {
disableBodyScroll(ref.current, { reserveScrollBarGap: true }) sidebarRef.current.focus()
} }
const contentElement = contentRef.current
if (contentElement) {
disableBodyScroll(contentElement, { reserveScrollBarGap: true })
}
return () => { return () => {
if (ref && ref.current) { if (contentElement) enableBodyScroll(contentElement)
enableBodyScroll(ref.current)
}
clearAllBodyScrollLocks() clearAllBodyScrollLocks()
} }
}, []) }, [])
return ( return (
<div className={cn(s.root)}> <div
className={cn(s.root)}
ref={sidebarRef}
onKeyDown={onKeyDownSidebar}
tabIndex={1}
>
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
<div className={s.backdrop} onClick={onClose} /> <div className={s.backdrop} onClick={onClose} />
<section className="absolute inset-y-0 right-0 max-w-full flex outline-none pl-10"> <section className="absolute inset-y-0 right-0 max-w-full flex outline-none pl-10">
<div className="h-full w-full md:w-screen md:max-w-md"> <div className="h-full w-full md:w-screen md:max-w-md">
<div className={s.sidebar} ref={ref}> <div className={s.sidebar} ref={contentRef}>
{children} {children}
</div> </div>
</div> </div>

View File

@ -13,3 +13,64 @@
.sectionHeading { .sectionHeading {
@apply pt-1 pb-2 text-2xl font-bold tracking-wide cursor-pointer mb-2; @apply pt-1 pb-2 text-2xl font-bold tracking-wide cursor-pointer mb-2;
} }
/* Apply base font sizes and styles for typography markup (h2, h2, ul, p, etc.).
A helpful addition for whenn page content is consumed from a source managed through a wysiwyg editor. */
.body :is(h1, h2, h3, h4, h5, h6, p, ul, ol) {
@apply mb-4;
}
.body :is(h1, h2, h3, h4, h5, h6):not(:first-child) {
@apply mt-8;
}
.body :is(h1, h2, h3, h4, h5, h6) {
@apply font-semibold tracking-wide;
}
.body h1 {
@apply text-5xl;
}
.body h2 {
@apply text-4xl;
}
.body h3 {
@apply text-3xl;
}
.body h4 {
@apply text-2xl;
}
.body h5 {
@apply text-xl;
}
.body h6 {
@apply text-lg;
}
.body ul,
.body ol {
@apply pl-6;
}
.body ul {
@apply list-disc;
}
.body ol {
@apply list-decimal;
}
.body a {
@apply underline;
}
.body a:hover {
@apply no-underline;
}

View File

@ -1,30 +1,58 @@
import type { CheckoutEndpoint } from '.' import type { CheckoutEndpoint } from '.'
import getCustomerId from '../../utils/get-customer-id'
import jwt from 'jsonwebtoken'
import { uuid } from 'uuidv4'
const fullCheckout = true const fullCheckout = true
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({ const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
req, req,
res, res,
config, config,
}) => { }) => {
const { cookies } = req const { cookies } = req
const cartId = cookies[config.cartCookie] const cartId = cookies[config.cartCookie]
const customerToken = cookies[config.customerCookie]
if (!cartId) { if (!cartId) {
res.redirect('/cart') res.redirect('/cart')
return return
} }
const { data } = await config.storeApiFetch( const { data } = await config.storeApiFetch(
`/v3/carts/${cartId}/redirect_urls`, `/v3/carts/${cartId}/redirect_urls`,
{ {
method: 'POST', method: 'POST',
} }
) )
const customerId =
customerToken && (await getCustomerId({ customerToken, config }))
if (fullCheckout) { //if there is a customer create a jwt token
res.redirect(data.checkout_url) if (!customerId) {
return if (fullCheckout) {
res.redirect(data.checkout_url)
return
}
} else {
const dateCreated = Math.round(new Date().getTime() / 1000)
const payload = {
iss: config.storeApiClientId,
iat: dateCreated,
jti: uuid(),
operation: 'customer_login',
store_hash: config.storeHash,
customer_id: customerId,
channel_id: config.storeChannelId,
redirect_to: data.checkout_url,
}
let token = jwt.sign(payload, config.storeApiClientSecret!, {
algorithm: 'HS256',
})
let checkouturl = `${config.storeUrl}/login/token/${token}`
console.log('checkouturl', checkouturl)
if (fullCheckout) {
res.redirect(checkouturl)
return
}
} }
// TODO: make the embedded checkout work too! // TODO: make the embedded checkout work too!
@ -59,4 +87,4 @@ const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
res.end() res.end()
} }
export default checkout export default getCheckout

View File

@ -2,13 +2,13 @@ import { GetAPISchema, createEndpoint } from '@commerce/api'
import checkoutEndpoint from '@commerce/api/endpoints/checkout' import checkoutEndpoint from '@commerce/api/endpoints/checkout'
import type { CheckoutSchema } from '../../../types/checkout' import type { CheckoutSchema } from '../../../types/checkout'
import type { BigcommerceAPI } from '../..' import type { BigcommerceAPI } from '../..'
import checkout from './checkout' import getCheckout from './get-checkout'
export type CheckoutAPI = GetAPISchema<BigcommerceAPI, CheckoutSchema> export type CheckoutAPI = GetAPISchema<BigcommerceAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint'] export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = { checkout } export const handlers: CheckoutEndpoint['handlers'] = { getCheckout }
const checkoutApi = createEndpoint<CheckoutAPI>({ const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint, handler: checkoutEndpoint,

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -1,6 +1,6 @@
import getCustomerWishlist from '../../operations/get-customer-wishlist' import getCustomerWishlist from '../../operations/get-customer-wishlist'
import { parseWishlistItem } from '../../utils/parse-item' import { parseWishlistItem } from '../../utils/parse-item'
import getCustomerId from './utils/get-customer-id' import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
// Return wishlist info // Return wishlist info

View File

@ -1,6 +1,6 @@
import type { Wishlist } from '../../../types/wishlist' import type { Wishlist } from '../../../types/wishlist'
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
import getCustomerId from './utils/get-customer-id' import getCustomerId from '../../utils/get-customer-id'
import getCustomerWishlist from '../../operations/get-customer-wishlist' import getCustomerWishlist from '../../operations/get-customer-wishlist'
// Return wishlist info // Return wishlist info

View File

@ -1,6 +1,6 @@
import type { Wishlist } from '../../../types/wishlist' import type { Wishlist } from '../../../types/wishlist'
import getCustomerWishlist from '../../operations/get-customer-wishlist' import getCustomerWishlist from '../../operations/get-customer-wishlist'
import getCustomerId from './utils/get-customer-id' import getCustomerId from '../../utils/get-customer-id'
import type { WishlistEndpoint } from '.' import type { WishlistEndpoint } from '.'
// Return wishlist info // Return wishlist info

View File

@ -32,6 +32,9 @@ export interface BigcommerceConfig extends CommerceAPIConfig {
storeApiToken: string storeApiToken: string
storeApiClientId: string storeApiClientId: string
storeChannelId?: string storeChannelId?: string
storeUrl?: string
storeApiClientSecret?: string
storeHash?:string
storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T> storeApiFetch<T>(endpoint: string, options?: RequestInit): Promise<T>
} }
@ -41,6 +44,9 @@ const STORE_API_URL = process.env.BIGCOMMERCE_STORE_API_URL
const STORE_API_TOKEN = process.env.BIGCOMMERCE_STORE_API_TOKEN const STORE_API_TOKEN = process.env.BIGCOMMERCE_STORE_API_TOKEN
const STORE_API_CLIENT_ID = process.env.BIGCOMMERCE_STORE_API_CLIENT_ID const STORE_API_CLIENT_ID = process.env.BIGCOMMERCE_STORE_API_CLIENT_ID
const STORE_CHANNEL_ID = process.env.BIGCOMMERCE_CHANNEL_ID const STORE_CHANNEL_ID = process.env.BIGCOMMERCE_CHANNEL_ID
const STORE_URL = process.env.BIGCOMMERCE_STORE_URL
const CLIENT_SECRET = process.env.BIGCOMMERCE_STORE_API_CLIENT_SECRET
const STOREFRONT_HASH = process.env.BIGCOMMERCE_STORE_API_STORE_HASH
if (!API_URL) { if (!API_URL) {
throw new Error( throw new Error(
@ -75,6 +81,9 @@ const config: BigcommerceConfig = {
storeApiToken: STORE_API_TOKEN, storeApiToken: STORE_API_TOKEN,
storeApiClientId: STORE_API_CLIENT_ID, storeApiClientId: STORE_API_CLIENT_ID,
storeChannelId: STORE_CHANNEL_ID, storeChannelId: STORE_CHANNEL_ID,
storeUrl:STORE_URL,
storeApiClientSecret:CLIENT_SECRET,
storeHash: STOREFRONT_HASH,
storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()), storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()),
} }

View File

@ -53,7 +53,7 @@ export default function getCustomerWishlistOperation({
if (ids?.length) { if (ids?.length) {
const graphqlData = await commerce.getAllProducts({ const graphqlData = await commerce.getAllProducts({
variables: { first: 100, ids }, variables: { first: 50, ids },
config, config,
}) })
// Put the products in an object that we can use to get them by id // Put the products in an object that we can use to get them by id

View File

@ -1,5 +1,5 @@
import type { GetCustomerIdQuery } from '../../../../schema' import type { GetCustomerIdQuery } from '../../schema'
import type { BigcommerceConfig } from '../../..' import type { BigcommerceConfig } from '../'
export const getCustomerIdQuery = /* GraphQL */ ` export const getCustomerIdQuery = /* GraphQL */ `
query getCustomerId { query getCustomerId {

View File

@ -10,7 +10,7 @@ type BCCartItemBody = {
product_id: number product_id: number
variant_id: number variant_id: number
quantity?: number quantity?: number
option_selections?: OptionSelections option_selections?: OptionSelections[]
} }
export const parseWishlistItem = ( export const parseWishlistItem = (

View File

@ -16,7 +16,7 @@ export const handler: MutationHook<LoginHook> = {
if (!(email && password)) { if (!(email && password)) {
throw new CommerceError({ throw new CommerceError({
message: message:
'A first name, last name, email and password are required to login', 'An email and password are required to login',
}) })
} }

View File

@ -0,0 +1,14 @@
import { SWRHook } from '@commerce/utils/types'
import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout'
export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ useData }) =>
async (input) => ({}),
}

View File

@ -1,6 +1,7 @@
{ {
"provider": "bigcommerce", "provider": "bigcommerce",
"features": { "features": {
"wishlist": true "wishlist": true,
"customerAuth": true
} }
} }

View File

@ -0,0 +1,15 @@
import useAddItem, { UseAddItem } from '@commerce/customer/address/use-add-item'
import { MutationHook } from '@commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
}

View File

@ -0,0 +1,15 @@
import useAddItem, { UseAddItem } from '@commerce/customer/card/use-add-item'
import { MutationHook } from '@commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
}

View File

@ -1,36 +1,9 @@
import type { ReactNode } from 'react' import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
import { import { bigcommerceProvider, BigcommerceProvider } from './provider'
CommerceConfig,
CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce,
} from '@commerce'
import { bigcommerceProvider } from './provider'
import type { BigcommerceProvider } from './provider'
export { bigcommerceProvider } export { bigcommerceProvider }
export type { BigcommerceProvider } export type { BigcommerceProvider }
export const bigcommerceConfig: CommerceConfig = { export const CommerceProvider = getCommerceProvider(bigcommerceProvider)
locale: 'en-us',
cartCookie: 'bc_cartId',
}
export type BigcommerceConfig = Partial<CommerceConfig>
export type BigcommerceProps = {
children?: ReactNode
locale: string
} & BigcommerceConfig
export function CommerceProvider({ children, ...config }: BigcommerceProps) {
return (
<CoreCommerceProvider
provider={bigcommerceProvider}
config={{ ...bigcommerceConfig, ...config }}
>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>() export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()

View File

@ -10,7 +10,7 @@ function normalizeProductOption(productOption: any) {
const { const {
node: { node: {
entityId, entityId,
values: { edges }, values: { edges = [] } = {},
...rest ...rest
}, },
} = productOption } = productOption
@ -91,7 +91,10 @@ export function normalizeCart(data: BigcommerceCart): Cart {
createdAt: data.created_time, createdAt: data.created_time,
currency: data.currency, currency: data.currency,
taxesIncluded: data.tax_included, taxesIncluded: data.tax_included,
lineItems: data.line_items.physical_items.map(normalizeLineItem), lineItems: [
...data.line_items.physical_items.map(normalizeLineItem),
...data.line_items.digital_items.map(normalizeLineItem),
],
lineItemsSubtotalPrice: data.base_amount, lineItemsSubtotalPrice: data.base_amount,
subtotalPrice: data.base_amount + data.discount_amount, subtotalPrice: data.base_amount + data.discount_amount,
totalPrice: data.cart_amount, totalPrice: data.cart_amount,

View File

@ -22,7 +22,7 @@ export const handler: SWRHook<SearchProductsHook> = {
const url = new URL(options.url!, 'http://a') const url = new URL(options.url!, 'http://a')
if (search) url.searchParams.set('search', search) if (search) url.searchParams.set('search', search)
if (Number.isInteger(categoryId)) if (Number.isInteger(Number(categoryId)))
url.searchParams.set('categoryId', String(categoryId)) url.searchParams.set('categoryId', String(categoryId))
if (Number.isInteger(brandId)) if (Number.isInteger(brandId))
url.searchParams.set('brandId', String(brandId)) url.searchParams.set('brandId', String(brandId))

View File

@ -40,7 +40,7 @@ export type OptionSelections = {
export type CartItemBody = Core.CartItemBody & { export type CartItemBody = Core.CartItemBody & {
productId: string // The product id is always required for BC productId: string // The product id is always required for BC
optionSelections?: OptionSelections optionSelections?: OptionSelections[]
} }
export type CartTypes = { export type CartTypes = {

View File

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

View File

@ -0,0 +1,65 @@
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
// Cart id might be usefull for anonymous shopping
const cartId = cookies[config.cartCookie]
try {
// Return customer addresses
if (req.method === 'GET') {
const body = { cartId }
return await handlers['getAddresses']({ ...ctx, body })
}
// Create or add an item to customer addresses list
if (req.method === 'POST') {
const body = { ...req.body, cartId }
return await handlers['addItem']({ ...ctx, body })
}
// Update item in customer addresses list
if (req.method === 'PUT') {
const body = { ...req.body, cartId }
return await handlers['updateItem']({ ...ctx, body })
}
// Remove an item from customer addresses list
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,65 @@
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
// Cart id might be usefull for anonymous shopping
const cartId = cookies[config.cartCookie]
try {
// Create or add a card
if (req.method === 'GET') {
const body = { ...req.body }
return await handlers['getCards']({ ...ctx, body })
}
// Create or add an item to customer cards
if (req.method === 'POST') {
const body = { ...req.body, cartId }
return await handlers['addItem']({ ...ctx, body })
}
// Update item in customer cards
if (req.method === 'PUT') {
const body = { ...req.body, cartId }
return await handlers['updateItem']({ ...ctx, body })
}
// Remove an item from customer cards
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,8 @@
import type { CustomerSchema } from '../../types/customer' import type { CustomerSchema } from '../../../types/customer'
import { CommerceAPIError } from '../utils/errors' import type { GetAPISchema } from '../..'
import isAllowedOperation from '../utils/is-allowed-operation'
import type { GetAPISchema } from '..' import { CommerceAPIError } from '../../utils/errors'
import isAllowedOperation from '../../utils/is-allowed-operation'
const customerEndpoint: GetAPISchema< const customerEndpoint: GetAPISchema<
any, any,

View File

@ -9,6 +9,8 @@ import type { SignupSchema } from '../types/signup'
import type { ProductsSchema } from '../types/product' import type { ProductsSchema } from '../types/product'
import type { WishlistSchema } from '../types/wishlist' import type { WishlistSchema } from '../types/wishlist'
import type { CheckoutSchema } from '../types/checkout' import type { CheckoutSchema } from '../types/checkout'
import type { CustomerCardSchema } from '../types/customer/card'
import type { CustomerAddressSchema } from '../types/customer/address'
import { import {
defaultOperations, defaultOperations,
OPERATIONS, OPERATIONS,
@ -25,6 +27,8 @@ export type APISchemas =
| ProductsSchema | ProductsSchema
| WishlistSchema | WishlistSchema
| CheckoutSchema | CheckoutSchema
| CustomerCardSchema
| CustomerAddressSchema
export type GetAPISchema< export type GetAPISchema<
C extends CommerceAPI<any>, C extends CommerceAPI<any>,
@ -61,8 +65,8 @@ export type EndpointHandlers<
[H in keyof E['handlers']]: APIHandler< [H in keyof E['handlers']]: APIHandler<
C, C,
EndpointHandlers<C, E>, EndpointHandlers<C, E>,
E['handlers'][H]['data'], NonNullable<E['handlers'][H]>['data'],
E['handlers'][H]['body'], NonNullable<E['handlers'][H]>['body'],
E['options'] E['options']
> >
} }

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 UseCheckout<
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?.useCheckout!
const useCheckout: UseCheckout = (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 useCheckout

View File

@ -0,0 +1,23 @@
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

@ -8,13 +8,15 @@ const merge = require('deepmerge')
const prettier = require('prettier') const prettier = require('prettier')
const PROVIDERS = [ const PROVIDERS = [
'local',
'bigcommerce', 'bigcommerce',
'saleor', 'saleor',
'shopify', 'shopify',
'swell', 'swell',
'vendure', 'vendure',
'local', 'local',
'elasticpath' 'elasticpath',
'ordercloud'
] ]
function getProviderName() { function getProviderName() {
@ -58,11 +60,27 @@ function withCommerceConfig(nextConfig = {}) {
// Update paths in `tsconfig.json` to point to the selected provider // Update paths in `tsconfig.json` to point to the selected provider
if (config.commerce.updateTSConfig !== false) { if (config.commerce.updateTSConfig !== false) {
const staticTsconfigPath = path.join(process.cwd(), 'tsconfig.json') const tsconfigPath = path.join(process.cwd(), 'tsconfig.json')
const tsconfig = require('../../tsconfig.js') const tsconfig = require(tsconfigPath)
tsconfig.compilerOptions.paths['@framework'] = [`framework/${name}`]
tsconfig.compilerOptions.paths['@framework/*'] = [`framework/${name}/*`]
// When running for production it may be useful to exclude the other providers
// from TS checking
if (process.env.VERCEL) {
const exclude = tsconfig.exclude.filter(
(item) => !item.startsWith('framework/')
)
tsconfig.exclude = PROVIDERS.reduce((exclude, current) => {
if (current !== name) exclude.push(`framework/${current}`)
return exclude
}, exclude)
}
fs.writeFileSync( fs.writeFileSync(
staticTsconfigPath, tsconfigPath,
prettier.format(JSON.stringify(tsconfig), { parser: 'json' }) prettier.format(JSON.stringify(tsconfig), { parser: 'json' })
) )
} }

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,34 @@
import type { SWRHook, HookFetcherFn } from '../../utils/types'
import type { GetAddressesHook } from '../../types/customer/address'
import Cookies from 'js-cookie'
import { useHook, useSWRHook } from '../../utils/use-hook'
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,21 @@
import type { HookFetcherFn, MutationHook } from '../../utils/types'
import type { RemoveItemHook } from '../../types/customer/address'
import type { Provider } from '../..'
import { useHook, useMutationHook } from '../../utils/use-hook'
import { mutationFetcher } from '../../utils/default-fetcher'
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,21 @@
import type { HookFetcherFn, MutationHook } from '../../utils/types'
import type { UpdateItemHook } from '../../types/customer/address'
import type { Provider } from '../..'
import { useHook, useMutationHook } from '../../utils/use-hook'
import { mutationFetcher } from '../../utils/default-fetcher'
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,34 @@
import type { SWRHook, HookFetcherFn } from '../../utils/types'
import type { GetCardsHook } from '../../types/customer/card'
import Cookies from 'js-cookie'
import { useHook, useSWRHook } from '../../utils/use-hook'
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,21 @@
import type { HookFetcherFn, MutationHook } from '../../utils/types'
import type { RemoveItemHook } from '../../types/customer/card'
import type { Provider } from '../..'
import { useHook, useMutationHook } from '../../utils/use-hook'
import { mutationFetcher } from '../../utils/default-fetcher'
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,21 @@
import type { HookFetcherFn, MutationHook } from '../../utils/types'
import type { UpdateItemHook } from '../../types/customer/card'
import type { Provider } from '../..'
import { useHook, useMutationHook } from '../../utils/use-hook'
import { mutationFetcher } from '../../utils/default-fetcher'
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, Signup,
Login, Login,
Logout, Logout,
Checkout,
} from '@commerce/types' } from '@commerce/types'
import type { Fetcher, SWRHook, MutationHook } from './utils/types' import type { Fetcher, SWRHook, MutationHook } from './utils/types'
@ -29,6 +30,10 @@ export type Provider = CommerceConfig & {
useUpdateItem?: MutationHook<Cart.UpdateItemHook> useUpdateItem?: MutationHook<Cart.UpdateItemHook>
useRemoveItem?: MutationHook<Cart.RemoveItemHook> useRemoveItem?: MutationHook<Cart.RemoveItemHook>
} }
checkout?: {
useCheckout?: SWRHook<Checkout.GetCheckoutHook>
useSubmitCheckout?: MutationHook<Checkout.SubmitCheckoutHook>
}
wishlist?: { wishlist?: {
useWishlist?: SWRHook<Wishlist.GetWishlistHook> useWishlist?: SWRHook<Wishlist.GetWishlistHook>
useAddItem?: MutationHook<Wishlist.AddItemHook> useAddItem?: MutationHook<Wishlist.AddItemHook>
@ -36,6 +41,18 @@ export type Provider = CommerceConfig & {
} }
customer?: { customer?: {
useCustomer?: SWRHook<Customer.CustomerHook> 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?: { products?: {
useSearch?: SWRHook<Product.SearchProductsHook> useSearch?: SWRHook<Product.SearchProductsHook>
@ -47,51 +64,60 @@ export type Provider = CommerceConfig & {
} }
} }
export type CommerceProps<P extends Provider> = { export type CommerceConfig = {
children?: ReactNode
provider: P
config: CommerceConfig
}
export type CommerceConfig = Omit<
CommerceContextValue<any>,
'providerRef' | 'fetcherRef'
>
export type CommerceContextValue<P extends Provider> = {
providerRef: MutableRefObject<P>
fetcherRef: MutableRefObject<Fetcher>
locale: string locale: string
cartCookie: string cartCookie: string
} }
export function CommerceProvider<P extends Provider>({ export type CommerceContextValue<P extends Provider> = {
providerRef: MutableRefObject<P>
fetcherRef: MutableRefObject<Fetcher>
} & CommerceConfig
export type CommerceProps<P extends Provider> = {
children?: ReactNode
provider: P
}
/**
* These are the properties every provider should allow when implementing
* the core commerce provider
*/
export type CommerceProviderProps = {
children?: ReactNode
} & Partial<CommerceConfig>
export function CoreCommerceProvider<P extends Provider>({
provider, provider,
children, children,
config,
}: CommerceProps<P>) { }: CommerceProps<P>) {
if (!config) {
throw new Error('CommerceProvider requires a valid config object')
}
const providerRef = useRef(provider) const providerRef = useRef(provider)
// TODO: Remove the fetcherRef // TODO: Remove the fetcherRef
const fetcherRef = useRef(provider.fetcher) const fetcherRef = useRef(provider.fetcher)
// Because the config is an object, if the parent re-renders this provider // If the parent re-renders this provider will re-render every
// will re-render every consumer unless we memoize the config // consumer unless we memoize the config
const { locale, cartCookie } = providerRef.current
const cfg = useMemo( const cfg = useMemo(
() => ({ () => ({ providerRef, fetcherRef, locale, cartCookie }),
providerRef, [locale, cartCookie]
fetcherRef,
locale: config.locale,
cartCookie: config.cartCookie,
}),
[config.locale, config.cartCookie]
) )
return <Commerce.Provider value={cfg}>{children}</Commerce.Provider> return <Commerce.Provider value={cfg}>{children}</Commerce.Provider>
} }
export function getCommerceProvider<P extends Provider>(provider: P) {
return function CommerceProvider({
children,
...props
}: CommerceProviderProps) {
return (
<CoreCommerceProvider provider={{ ...provider, ...props }}>
{children}
</CoreCommerceProvider>
)
}
}
export function useCommerce<P extends Provider>() { export function useCommerce<P extends Provider>() {
return useContext(Commerce) as CommerceContextValue<P> return useContext(Commerce) as CommerceContextValue<P>
} }

View File

@ -47,6 +47,12 @@ The app imports from the provider directly instead of the core commerce folder (
The provider folder should only depend on `framework/commerce` and dependencies in the main `package.json`. In the future we'll move the `framework` folder to a package that can be shared easily for multiple apps. The provider folder should only depend on `framework/commerce` and dependencies in the main `package.json`. In the future we'll move the `framework` folder to a package that can be shared easily for multiple apps.
## Updating the list of known providers
Open [./config.js](./config.js) and add the provider name to the list in `PROVIDERS`.
Then, open [/.env.template](/.env.template) and add the provider name in the first line.
## Adding the provider hooks ## Adding the provider hooks
Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks: Using BigCommerce as an example. The first thing to do is export a `CommerceProvider` component that includes a `provider` object with all the handlers that can be used for hooks:

View File

@ -1,10 +1,57 @@
export type CheckoutSchema = { import type { UseSubmitCheckout } from '../checkout/use-submit-checkout'
import type { Address } from './customer/address'
import type { Card } from './customer/card'
// Index
export type Checkout = any
export type CheckoutTypes = {
card?: Card
address?: Address
checkout?: Checkout
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 }
mutations: { submit: UseSubmitCheckout }
}
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: { endpoint: {
options: {} options: {}
handlers: { handlers: CheckoutHandlers<T>
checkout: {
data: null
}
}
} }
} }

View File

@ -0,0 +1,111 @@
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['address'] }
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 AddressHandler<
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,102 @@
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['card'] }
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 // TODO: define this type
export type Customer = any export type Customer = any

View File

@ -87,6 +87,8 @@ export type HookSchemaBase = {
export type SWRHookSchemaBase = HookSchemaBase & { export type SWRHookSchemaBase = HookSchemaBase & {
// Custom state added to the response object of SWR // Custom state added to the response object of SWR
swrState?: {} swrState?: {}
// Instances of MutationSchemaBase that the hook returns for better DX
mutations?: Record<string, ReturnType<MutationHook<any>['useHook']>>
} }
export type MutationSchemaBase = HookSchemaBase & { export type MutationSchemaBase = HookSchemaBase & {
@ -102,7 +104,7 @@ export type SWRHook<H extends SWRHookSchemaBase> = {
context: SWRHookContext<H> context: SWRHookContext<H>
): HookFunction< ): HookFunction<
H['input'] & { swrOptions?: SwrOptions<H['data'], H['fetcherInput']> }, H['input'] & { swrOptions?: SwrOptions<H['data'], H['fetcherInput']> },
ResponseState<H['data']> & H['swrState'] ResponseState<H['data']> & H['swrState'] & H['mutations']
> >
fetchOptions: HookFetcherOptions fetchOptions: HookFetcherOptions
fetcher?: HookFetcherFn<H> fetcher?: HookFetcherFn<H>

View File

@ -1,4 +1,4 @@
import useSWR, { responseInterface } from 'swr' import useSWR, { SWRResponse } from 'swr'
import type { import type {
HookSWRInput, HookSWRInput,
HookFetchInput, HookFetchInput,
@ -11,7 +11,7 @@ import type {
import defineProperty from './define-property' import defineProperty from './define-property'
import { CommerceError } from './errors' import { CommerceError } from './errors'
export type ResponseState<Result> = responseInterface<Result, CommerceError> & { export type ResponseState<Result> = SWRResponse<Result, CommerceError> & {
isLoading: boolean isLoading: boolean
} }
@ -72,7 +72,7 @@ const useData: UseData = (options, input, fetcherFn, swrOptions) => {
}) })
} }
return response return response as typeof response & { isLoading: boolean }
} }
export default useData export default useData

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1,14 @@
import { SWRHook } from '@commerce/utils/types'
import useCheckout, { UseCheckout } from '@commerce/checkout/use-checkout'
export default useCheckout as UseCheckout<typeof handler>
export const handler: SWRHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ useData }) =>
async (input) => ({}),
}

View File

@ -1,6 +1,9 @@
{ {
"provider": "local", "provider": "local",
"features": { "features": {
"wishlist": false "wishlist": false,
"cart": false,
"search": false,
"customerAuth": false
} }
} }

View File

@ -0,0 +1,15 @@
import useAddItem, { UseAddItem } from '@commerce/customer/address/use-add-item'
import { MutationHook } from '@commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
}

View File

@ -0,0 +1,15 @@
import useAddItem, { UseAddItem } from '@commerce/customer/card/use-add-item'
import { MutationHook } from '@commerce/utils/types'
export default useAddItem as UseAddItem<typeof handler>
export const handler: MutationHook<any> = {
fetchOptions: {
query: '',
},
async fetcher({ input, options, fetch }) {},
useHook:
({ fetch }) =>
() =>
async () => ({}),
}

View File

@ -1,32 +1,9 @@
import * as React from 'react' import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
import { ReactNode } from 'react' import { localProvider, LocalProvider } from './provider'
import { localProvider } from './provider'
import {
CommerceConfig,
CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce,
} from '@commerce'
export const localConfig: CommerceConfig = { export { localProvider }
locale: 'en-us', export type { LocalProvider }
cartCookie: 'session',
}
export function CommerceProvider({ export const CommerceProvider = getCommerceProvider(localProvider)
children,
...config
}: {
children?: ReactNode
locale: string
} & Partial<CommerceConfig>) {
return (
<CoreCommerceProvider
provider={localProvider}
config={{ ...localConfig, ...config }}
>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce() export const useCommerce = () => useCoreCommerce<LocalProvider>()

View File

@ -9,7 +9,6 @@ import { handler as useLogin } from './auth/use-login'
import { handler as useLogout } from './auth/use-logout' import { handler as useLogout } from './auth/use-logout'
import { handler as useSignup } from './auth/use-signup' import { handler as useSignup } from './auth/use-signup'
export type Provider = typeof localProvider
export const localProvider = { export const localProvider = {
locale: 'en-us', locale: 'en-us',
cartCookie: 'session', cartCookie: 'session',
@ -19,3 +18,5 @@ export const localProvider = {
products: { useSearch }, products: { useSearch },
auth: { useLogin, useLogout, useSignup }, auth: { useLogin, useLogout, useSignup },
} }
export type LocalProvider = typeof localProvider

View File

@ -0,0 +1,5 @@
COMMERCE_PROVIDER=ordercloud
ORDERCLOUD_CLIENT_ID=
ORDERCLOUD_CLIENT_SECRET=
STRIPE_SECRET=

View File

@ -0,0 +1,3 @@
# Next.js Ordercloud Provider
Create your own store from [here](https://nextjs.org/commerce)

View File

@ -0,0 +1,99 @@
import type { CartEndpoint } from '.'
import type { RawVariant } from '../../../types/product'
import type { OrdercloudLineItem } from '../../../types/cart'
import { serialize } from 'cookie'
import { formatCart } from '../../utils/cart'
const addItem: CartEndpoint['handlers']['addItem'] = async ({
res,
body: { cartId, item },
config: { restBuyerFetch, cartCookie, tokenCookie },
}) => {
// Return an error if no item is present
if (!item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing item' }],
})
}
// Store token
let token
// Set the quantity if not present
if (!item.quantity) item.quantity = 1
// Create an order if it doesn't exist
if (!cartId) {
const { ID, meta } = await restBuyerFetch(
'POST',
`/orders/Outgoing`,
{}
).then((response: { ID: string; meta: { token: string } }) => response)
// Set the cart id and token
cartId = ID
token = meta.token
// Set the cart and token cookie
res.setHeader('Set-Cookie', [
serialize(tokenCookie, meta.token, {
maxAge: 60 * 60 * 24 * 30,
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
}),
serialize(cartCookie, cartId, {
maxAge: 60 * 60 * 24 * 30,
expires: new Date(Date.now() + 60 * 60 * 24 * 30 * 1000),
secure: process.env.NODE_ENV === 'production',
path: '/',
sameSite: 'lax',
}),
])
}
// Store specs
let specs: RawVariant['Specs'] = []
// If a variant is present, fetch its specs
if (item.variantId) {
specs = await restBuyerFetch(
'GET',
`/me/products/${item.productId}/variants/${item.variantId}`,
null,
{ token }
).then((res: RawVariant) => res.Specs)
}
// Add the item to the order
await restBuyerFetch(
'POST',
`/orders/Outgoing/${cartId}/lineitems`,
{
ProductID: item.productId,
Quantity: item.quantity,
Specs: specs,
},
{ token }
)
// Get cart
const [cart, lineItems] = await Promise.all([
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
token,
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
])
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
}
export default addItem

View File

@ -0,0 +1,65 @@
import type { OrdercloudLineItem } from '../../../types/cart'
import type { CartEndpoint } from '.'
import { serialize } from 'cookie'
import { formatCart } from '../../utils/cart'
// Return current cart info
const getCart: CartEndpoint['handlers']['getCart'] = async ({
req,
res,
body: { cartId },
config: { restBuyerFetch, cartCookie, tokenCookie },
}) => {
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
try {
// Get token from cookies
const token = req.cookies[tokenCookie]
// Get cart
const cart = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}`,
null,
{ token }
)
// Get line items
const lineItems = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}/lineitems`,
null,
{ token }
).then((response: { Items: OrdercloudLineItem[] }) => response.Items)
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
} catch (error) {
// Reset cart and token cookie
res.setHeader('Set-Cookie', [
serialize(cartCookie, cartId, {
maxAge: -1,
path: '/',
}),
serialize(tokenCookie, cartId, {
maxAge: -1,
path: '/',
}),
])
// Return empty cart
res.status(200).json({ data: null, errors: [] })
}
}
export default getCart

View File

@ -0,0 +1,28 @@
import type { CartSchema } from '../../../types/cart'
import type { OrdercloudAPI } from '../..'
import { GetAPISchema, createEndpoint } from '@commerce/api'
import cartEndpoint from '@commerce/api/endpoints/cart'
import getCart from './get-cart'
import addItem from './add-item'
import updateItem from './update-item'
import removeItem from './remove-item'
export type CartAPI = GetAPISchema<OrdercloudAPI, CartSchema>
export type CartEndpoint = CartAPI['endpoint']
export const handlers: CartEndpoint['handlers'] = {
getCart,
addItem,
updateItem,
removeItem,
}
const cartApi = createEndpoint<CartAPI>({
handler: cartEndpoint,
handlers,
})
export default cartApi

View File

@ -0,0 +1,45 @@
import type { CartEndpoint } from '.'
import { formatCart } from '../../utils/cart'
import { OrdercloudLineItem } from '../../../types/cart'
const removeItem: CartEndpoint['handlers']['removeItem'] = async ({
req,
res,
body: { cartId, itemId },
config: { restBuyerFetch, tokenCookie },
}) => {
if (!cartId || !itemId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Remove the item to the order
await restBuyerFetch(
'DELETE',
`/orders/Outgoing/${cartId}/lineitems/${itemId}`,
null,
{ token }
)
// Get cart
const [cart, lineItems] = await Promise.all([
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
token,
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
])
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
}
export default removeItem

View File

@ -0,0 +1,63 @@
import type { OrdercloudLineItem } from '../../../types/cart'
import type { RawVariant } from '../../../types/product'
import type { CartEndpoint } from '.'
import { formatCart } from '../../utils/cart'
const updateItem: CartEndpoint['handlers']['updateItem'] = async ({
req,
res,
body: { cartId, itemId, item },
config: { restBuyerFetch, tokenCookie },
}) => {
if (!cartId || !itemId || !item) {
return res.status(400).json({
data: null,
errors: [{ message: 'Invalid request' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Store specs
let specs: RawVariant['Specs'] = []
// If a variant is present, fetch its specs
if (item.variantId) {
specs = await restBuyerFetch(
'GET',
`/me/products/${item.productId}/variants/${item.variantId}`,
null,
{ token }
).then((res: RawVariant) => res.Specs)
}
// Add the item to the order
await restBuyerFetch(
'PATCH',
`/orders/Outgoing/${cartId}/lineitems/${itemId}`,
{
ProductID: item.productId,
Quantity: item.quantity,
Specs: specs,
},
{ token }
)
// Get cart
const [cart, lineItems] = await Promise.all([
restBuyerFetch('GET', `/orders/Outgoing/${cartId}`, null, { token }),
restBuyerFetch('GET', `/orders/Outgoing/${cartId}/lineitems`, null, {
token,
}).then((response: { Items: OrdercloudLineItem[] }) => response.Items),
])
// Format cart
const formattedCart = formatCart(cart, lineItems)
// Return cart and errors
res.status(200).json({ data: formattedCart, errors: [] })
}
export default updateItem

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1 @@
export default function noopApi(...args: any[]): void {}

View File

@ -0,0 +1,47 @@
import type { CheckoutEndpoint } from '.'
const getCheckout: CheckoutEndpoint['handlers']['getCheckout'] = async ({
req,
res,
body: { cartId },
config: { restBuyerFetch, tokenCookie },
}) => {
// Return an error if no item is present
if (!cartId) {
return res.status(400).json({
data: null,
errors: [{ message: 'Missing cookie' }],
})
}
// Get token from cookies
const token = req.cookies[tokenCookie]
// Register credit card
const payments = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}/payments`,
null,
{ token }
).then((response: { Items: unknown[] }) => response.Items)
const address = await restBuyerFetch(
'GET',
`/orders/Outgoing/${cartId}`,
null,
{ token }
).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

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

View File

@ -0,0 +1,47 @@
import type { CustomerAddressEndpoint } from '.'
const addItem: CustomerAddressEndpoint['handlers']['addItem'] = async ({
res,
body: { item, cartId },
config: { restBuyerFetch },
}) => {
// 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 restBuyerFetch('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 restBuyerFetch('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

Some files were not shown because too many files have changed in this diff Show More