Merge branch 'main' into vendure-fetcher-errors

This commit is contained in:
Luis Alvarez D
2021-09-22 19:32:43 -05:00
committed by GitHub
131 changed files with 2574 additions and 653 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,6 @@ 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=

6
.eslintrc Normal file
View File

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

View File

@@ -79,7 +79,7 @@ For example: Turning `cart` off will disable Cart capabilities.
> NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out the box) > NOTE: The selected provider should support the feature that you are toggling. (This means that you can't turn wishlist on if the provider doesn't support this functionality out the box)
- Open `commerce.config.json` - Open `commerce.config.json`
- You'll see a config file like this: - You'll see a config file like this:
```json ```json
{ {
@@ -151,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 (

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,
} }
) )
@@ -38,22 +47,31 @@ const CheckoutSidebarView: FC = () => {
<Text variant="sectionHeading">Checkout</Text> <Text variant="sectionHeading">Checkout</Text>
</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 +92,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,14 +1,15 @@
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. */
return ( return (
<div onClick={onClick} className={s.root}> <div onClick={onClick} className={s.root}>
@@ -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,15 +1,16 @@
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. */
return ( return (
<div onClick={onClick} className={s.root}> <div onClick={onClick} className={s.root}>
@@ -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

@@ -73,7 +73,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
<div className="flex items-center text-primary text-sm"> <div className="flex items-center text-primary text-sm">
<span className="text-primary">Created by</span> <span className="text-primary">Created by</span>
<a <a
rel="noopener" rel="noopener noreferrer"
href="https://vercel.com" href="https://vercel.com"
aria-label="Vercel.com Link" aria-label="Vercel.com Link"
target="_blank" target="_blank"

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,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'
@@ -26,9 +27,11 @@ const UserNav: FC<Props> = ({ className }) => {
<nav className={cn(s.root, className)}> <nav className={cn(s.root, className)}>
<ul className={s.list}> <ul className={s.list}>
{process.env.COMMERCE_CART_ENABLED && ( {process.env.COMMERCE_CART_ENABLED && (
<li className={s.item} onClick={toggleSidebar}> <li className={s.item}>
<Bag /> <Button className={s.item} variant="naked" onClick={toggleSidebar} aria-label="Cart">
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>} <Bag />
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
</Button>
</li> </li>
)} )}
{process.env.COMMERCE_WISHLIST_ENABLED && ( {process.env.COMMERCE_WISHLIST_ENABLED && (

View File

@@ -7,6 +7,7 @@ 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
@@ -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 () => {

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

@@ -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

@@ -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 submitCheckout from './submit-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'] = { submitCheckout }
const checkoutApi = createEndpoint<CheckoutAPI>({ const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint, handler: checkoutEndpoint,

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 submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = 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 submitCheckout

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

@@ -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

@@ -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

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') {
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>,

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,12 +8,12 @@ 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',
] ]
function getProviderName() { function getProviderName() {

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

View File

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

View File

@@ -1,3 +1,6 @@
export * as Card from "./card"
export * as Address from "./address"
// TODO: define this type // 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

@@ -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

@@ -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

@@ -1,19 +1,22 @@
## Saleor Provider ## Saleor Provider
**Demo:** TBD **Demo:** https://saleor.vercel.store/
Before getting starter, a [Saleor](https://saleor.io/) account and store is required before using the provider. You need a [Saleor](https://saleor.io/) instance, either in the cloud or self-hosted.
Next, copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git): This provider requires Saleor **3.x** or higher.
Copy the `.env.template` file in this directory to `.env.local` in the main directory (which will be ignored by Git):
```bash ```bash
cp framework/saleor/.env.template .env.local cp framework/saleor/.env.template .env.local
``` ```
Then, set the environment variables in `.env.local` to match the ones from your store. Then, set the environment following variables in your `.env.local`. Both, `NEXT_PUBLIC_SALEOR_API_URL` and `COMMERCE_IMAGE_HOST` must point to your own Saleor instance.
## Contribute ```
COMMERCE_PROVIDER=saleor
Our commitment to Open Source can be found [here](https://vercel.com/oss). NEXT_PUBLIC_SALEOR_API_URL=https://vercel.saleor.cloud/graphql/
NEXT_PUBLIC_SALEOR_CHANNEL=default-channel
If you find an issue with the provider or want a new feature, feel free to open a PR or [create a new issue](https://github.com/vercel/commerce/issues). COMMERCE_IMAGE_HOST=vercel.saleor.cloud
```

View File

@@ -6,11 +6,7 @@ export type CheckoutAPI = GetAPISchema<CommerceAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint'] export type CheckoutEndpoint = CheckoutAPI['endpoint']
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({ const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({ req, res, config }) => {
req,
res,
config,
}) => {
try { try {
const html = ` const html = `
<!DOCTYPE html> <!DOCTYPE html>
@@ -47,7 +43,7 @@ const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
} }
} }
export const handlers: CheckoutEndpoint['handlers'] = { checkout } export const handlers: CheckoutEndpoint['handlers'] = { submitCheckout }
const checkoutApi = createEndpoint<CheckoutAPI>({ const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint, handler: checkoutEndpoint,

View File

@@ -1 +0,0 @@
export default function (_commerce: any) {}

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 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -1,5 +1,5 @@
import type { CommerceAPIConfig } from '@commerce/api' import { CommerceAPI, CommerceAPIConfig, getCommerceApi as commerceApi } from '@commerce/api'
import * as operations from './operations'
import * as Const from '../const' import * as Const from '../const'
if (!Const.API_URL) { if (!Const.API_URL) {
@@ -27,23 +27,12 @@ const config: SaleorConfig = {
storeChannel: Const.API_CHANNEL, storeChannel: Const.API_CHANNEL,
} }
import {
CommerceAPI,
getCommerceApi as commerceApi,
} from '@commerce/api'
import * as operations from './operations'
export interface ShopifyConfig extends CommerceAPIConfig {}
export const provider = { config, operations } export const provider = { config, operations }
export type Provider = typeof provider export type Provider = typeof provider
export type SaleorAPI<P extends Provider = Provider> = CommerceAPI<P> export type SaleorAPI<P extends Provider = Provider> = CommerceAPI<P>
export function getCommerceApi<P extends Provider>( export function getCommerceApi<P extends Provider>(customProvider: P = provider as any): SaleorAPI<P> {
customProvider: P = provider as any
): SaleorAPI<P> {
return commerceApi(customProvider) return commerceApi(customProvider)
} }

View File

@@ -1,10 +1,6 @@
import type { OperationContext } from '@commerce/api/operations' import type { OperationContext } from '@commerce/api/operations'
import { import { ProductCountableEdge } from '../../schema'
GetAllProductPathsQuery, import type { Provider, SaleorConfig } from '..'
GetAllProductPathsQueryVariables,
ProductCountableEdge,
} from '../../schema'
import type { ShopifyConfig, Provider, SaleorConfig } from '..'
import { getAllProductsPathsQuery } from '../../utils/queries' import { getAllProductsPathsQuery } from '../../utils/queries'
import fetchAllProducts from '../utils/fetch-all-products' import fetchAllProducts from '../utils/fetch-all-products'
@@ -13,10 +9,7 @@ export type GetAllProductPathsResult = {
products: Array<{ path: string }> products: Array<{ path: string }>
} }
export default function getAllProductPathsOperation({ export default function getAllProductPathsOperation({ commerce }: OperationContext<Provider>) {
commerce,
}: OperationContext<Provider>) {
async function getAllProductPaths({ async function getAllProductPaths({
query, query,
config, config,
@@ -24,7 +17,7 @@ export default function getAllProductPathsOperation({
}: { }: {
query?: string query?: string
config?: SaleorConfig config?: SaleorConfig
variables?: any variables?: any
} = {}): Promise<GetAllProductPathsResult> { } = {}): Promise<GetAllProductPathsResult> {
config = commerce.getConfig(config) config = commerce.getConfig(config)
@@ -39,7 +32,6 @@ export default function getAllProductPathsOperation({
path: `/${slug}`, path: `/${slug}`,
})), })),
} }
} }
return getAllProductPaths return getAllProductPaths

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

@@ -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 { CommerceConfig, CommerceProvider as CoreCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
import { saleorProvider, SaleorProvider } from './provider' import { saleorProvider, SaleorProvider } from './provider'
import * as Const from './const'
export { saleorProvider } export { saleorProvider }
export type { SaleorProvider } export type { SaleorProvider }
export const saleorConfig: CommerceConfig = { export const CommerceProvider = getCommerceProvider(saleorProvider)
locale: 'en-us',
cartCookie: Const.CHECKOUT_ID_COOKIE,
}
export type SaleorConfig = Partial<CommerceConfig> export const useCommerce = () => useCoreCommerce<SaleorProvider>()
export type SaleorProps = {
children?: ReactNode
locale: string
} & SaleorConfig
export function CommerceProvider({ children, ...config }: SaleorProps) {
return (
<CoreCommerceProvider provider={saleorProvider} config={{ ...saleorConfig, ...config }}>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce()

View File

@@ -1,3 +1,4 @@
import { CHECKOUT_ID_COOKIE } from './const'
import { handler as useCart } from './cart/use-cart' import { handler as useCart } from './cart/use-cart'
import { handler as useAddItem } from './cart/use-add-item' import { handler as useAddItem } from './cart/use-add-item'
import { handler as useUpdateItem } from './cart/use-update-item' import { handler as useUpdateItem } from './cart/use-update-item'
@@ -14,8 +15,7 @@ import fetcher from './fetcher'
export const saleorProvider = { export const saleorProvider = {
locale: 'en-us', locale: 'en-us',
cartCookie: '', cartCookie: CHECKOUT_ID_COOKIE,
cartCookieToken: '',
fetcher, fetcher,
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem }, cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
customer: { useCustomer }, customer: { useCustomer },

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 { ShopifyAPI } from '../..' import type { ShopifyAPI } from '../..'
import checkout from './checkout' import submitCheckout from './submit-checkout'
export type CheckoutAPI = GetAPISchema<ShopifyAPI, CheckoutSchema> export type CheckoutAPI = GetAPISchema<ShopifyAPI, CheckoutSchema>
export type CheckoutEndpoint = CheckoutAPI['endpoint'] export type CheckoutEndpoint = CheckoutAPI['endpoint']
export const handlers: CheckoutEndpoint['handlers'] = { checkout } export const handlers: CheckoutEndpoint['handlers'] = { submitCheckout }
const checkoutApi = createEndpoint<CheckoutAPI>({ const checkoutApi = createEndpoint<CheckoutAPI>({
handler: checkoutEndpoint, handler: checkoutEndpoint,

View File

@@ -6,7 +6,7 @@ import {
import associateCustomerWithCheckoutMutation from '../../../utils/mutations/associate-customer-with-checkout' import associateCustomerWithCheckoutMutation from '../../../utils/mutations/associate-customer-with-checkout'
import type { CheckoutEndpoint } from '.' import type { CheckoutEndpoint } from '.'
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({ const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
req, req,
res, res,
config, config,
@@ -35,4 +35,4 @@ const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
} }
} }
export default checkout export default submitCheckout

View File

@@ -1 +0,0 @@
export default function (_commerce: any) {}

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 @@
export default function noopApi(...args: any[]): void {}

View File

@@ -22,7 +22,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,6 @@
{ {
"schema": { "schema": {
"https://${NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN}/api/2021-01/graphql.json": { "https://${NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN}/api/2021-07/graphql.json": {
"headers": { "headers": {
"X-Shopify-Storefront-Access-Token": "${NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN}" "X-Shopify-Storefront-Access-Token": "${NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN}"
} }

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,40 +1,9 @@
import * as React from 'react' import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
import { ReactNode } from 'react' import { shopifyProvider, ShopifyProvider } from './provider'
import {
CommerceConfig,
CommerceProvider as CoreCommerceProvider,
useCommerce as useCoreCommerce,
} from '@commerce'
import { shopifyProvider } from './provider'
import type { ShopifyProvider } from './provider'
import { SHOPIFY_CHECKOUT_ID_COOKIE } from './const'
export { shopifyProvider } export { shopifyProvider }
export type { ShopifyProvider } export type { ShopifyProvider }
export const shopifyConfig: CommerceConfig = { export const CommerceProvider = getCommerceProvider(shopifyProvider)
locale: 'en-us',
cartCookie: SHOPIFY_CHECKOUT_ID_COOKIE,
}
export type ShopifyConfig = Partial<CommerceConfig>
export type ShopifyProps = {
children?: ReactNode
locale: string
} & ShopifyConfig
export function CommerceProvider({ children, ...config }: ShopifyProps) {
return (
<CoreCommerceProvider
provider={shopifyProvider}
config={{ ...shopifyConfig, ...config }}
>
{children}
</CoreCommerceProvider>
)
}
export const useCommerce = () => useCoreCommerce<ShopifyProvider>() export const useCommerce = () => useCoreCommerce<ShopifyProvider>()

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