mirror of
https://github.com/vercel/commerce.git
synced 2025-07-22 20:26:49 +00:00
Merge branch 'main' into issue/382-tyopgraphy-formatting-text-component
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# Available providers: bigcommerce, shopify, swell
|
||||
# Available providers: local, bigcommerce, shopify, swell, saleor
|
||||
COMMERCE_PROVIDER=
|
||||
|
||||
BIGCOMMERCE_STOREFRONT_API_URL=
|
||||
@@ -7,6 +7,10 @@ BIGCOMMERCE_STORE_API_URL=
|
||||
BIGCOMMERCE_STORE_API_TOKEN=
|
||||
BIGCOMMERCE_STORE_API_CLIENT_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_STOREFRONT_ACCESS_TOKEN=
|
||||
@@ -16,3 +20,6 @@ NEXT_PUBLIC_SWELL_PUBLIC_KEY=
|
||||
|
||||
NEXT_PUBLIC_SALEOR_API_URL=
|
||||
NEXT_PUBLIC_SALEOR_CHANNEL=
|
||||
|
||||
NEXT_PUBLIC_VENDURE_SHOP_API_URL=
|
||||
NEXT_PUBLIC_VENDURE_LOCAL_URL=
|
||||
|
6
.eslintrc
Normal file
6
.eslintrc
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": ["next", "prettier"],
|
||||
"rules": {
|
||||
"react/no-unescaped-entities": "off"
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
||||
- Open `commerce.config.json`
|
||||
- Open `commerce.config.json`
|
||||
- You'll see a config file like this:
|
||||
```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.
|
||||
<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>
|
||||
|
@@ -77,7 +77,6 @@ html {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
touch-action: manipulation;
|
||||
font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
@@ -38,6 +38,7 @@ const LoginView: FC<Props> = () => {
|
||||
} catch ({ errors }) {
|
||||
setMessage(errors[0].message)
|
||||
setLoading(false)
|
||||
setDisabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -70,6 +70,9 @@ const CartItem = ({
|
||||
if (item.quantity !== Number(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])
|
||||
|
||||
return (
|
||||
|
@@ -1,30 +1,39 @@
|
||||
import cn from 'classnames'
|
||||
import Link from 'next/link'
|
||||
import { FC } from 'react'
|
||||
import CartItem from '@components/cart/CartItem'
|
||||
import { Button, Text } from '@components/ui'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import SidebarLayout from '@components/common/SidebarLayout'
|
||||
import useCart from '@framework/cart/use-cart'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import useCheckout from '@framework/checkout/use-checkout'
|
||||
import ShippingWidget from '../ShippingWidget'
|
||||
import PaymentWidget from '../PaymentWidget'
|
||||
import SidebarLayout from '@components/common/SidebarLayout'
|
||||
import s from './CheckoutSidebarView.module.css'
|
||||
|
||||
const CheckoutSidebarView: FC = () => {
|
||||
const { setSidebarView } = useUI()
|
||||
const { data } = useCart()
|
||||
const { setSidebarView, closeSidebar } = useUI()
|
||||
const { data: cartData } = useCart()
|
||||
const { data: checkoutData, submit: onCheckout } = useCheckout()
|
||||
|
||||
async function handleSubmit(event: React.ChangeEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
await onCheckout()
|
||||
|
||||
closeSidebar()
|
||||
}
|
||||
|
||||
const { price: subTotal } = usePrice(
|
||||
data && {
|
||||
amount: Number(data.subtotalPrice),
|
||||
currencyCode: data.currency.code,
|
||||
cartData && {
|
||||
amount: Number(cartData.subtotalPrice),
|
||||
currencyCode: cartData.currency.code,
|
||||
}
|
||||
)
|
||||
const { price: total } = usePrice(
|
||||
data && {
|
||||
amount: Number(data.totalPrice),
|
||||
currencyCode: data.currency.code,
|
||||
cartData && {
|
||||
amount: Number(cartData.totalPrice),
|
||||
currencyCode: cartData.currency.code,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -38,22 +47,31 @@ const CheckoutSidebarView: FC = () => {
|
||||
<Text variant="sectionHeading">Checkout</Text>
|
||||
</Link>
|
||||
|
||||
<PaymentWidget onClick={() => setSidebarView('PAYMENT_VIEW')} />
|
||||
<ShippingWidget onClick={() => setSidebarView('SHIPPING_VIEW')} />
|
||||
<PaymentWidget
|
||||
isValid={checkoutData?.hasPayment}
|
||||
onClick={() => setSidebarView('PAYMENT_VIEW')}
|
||||
/>
|
||||
<ShippingWidget
|
||||
isValid={checkoutData?.hasShipping}
|
||||
onClick={() => setSidebarView('SHIPPING_VIEW')}
|
||||
/>
|
||||
|
||||
<ul className={s.lineItemsList}>
|
||||
{data!.lineItems.map((item: any) => (
|
||||
{cartData!.lineItems.map((item: any) => (
|
||||
<CartItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
currencyCode={data!.currency.code}
|
||||
currencyCode={cartData!.currency.code}
|
||||
variant="display"
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accent-0 border-t text-sm">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex-shrink-0 px-6 py-6 sm:px-6 sticky z-20 bottom-0 w-full right-0 left-0 bg-accent-0 border-t text-sm"
|
||||
>
|
||||
<ul className="pb-2">
|
||||
<li className="flex justify-between py-1">
|
||||
<span>Subtotal</span>
|
||||
@@ -74,14 +92,15 @@ const CheckoutSidebarView: FC = () => {
|
||||
</div>
|
||||
<div>
|
||||
{/* Once data is correcly filled */}
|
||||
{/* <Button Component="a" width="100%">
|
||||
Confirm Purchase
|
||||
</Button> */}
|
||||
<Button Component="a" width="100%" variant="ghost" disabled>
|
||||
Continue
|
||||
<Button
|
||||
type="submit"
|
||||
width="100%"
|
||||
disabled={!checkoutData?.hasPayment || !checkoutData?.hasShipping}
|
||||
>
|
||||
Confirm Purchase
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</SidebarLayout>
|
||||
)
|
||||
}
|
||||
|
@@ -1,83 +1,129 @@
|
||||
import { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
|
||||
import useAddCard from '@framework/customer/card/use-add-item'
|
||||
import { Button, Text } from '@components/ui'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import s from './PaymentMethodView.module.css'
|
||||
import SidebarLayout from '@components/common/SidebarLayout'
|
||||
|
||||
import s from './PaymentMethodView.module.css'
|
||||
|
||||
interface Form extends HTMLFormElement {
|
||||
cardHolder: HTMLInputElement
|
||||
cardNumber: HTMLInputElement
|
||||
cardExpireDate: HTMLInputElement
|
||||
cardCvc: HTMLInputElement
|
||||
firstName: HTMLInputElement
|
||||
lastName: HTMLInputElement
|
||||
company: HTMLInputElement
|
||||
streetNumber: HTMLInputElement
|
||||
zipCode: HTMLInputElement
|
||||
city: HTMLInputElement
|
||||
country: HTMLSelectElement
|
||||
}
|
||||
|
||||
const PaymentMethodView: FC = () => {
|
||||
const { setSidebarView } = useUI()
|
||||
const 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 (
|
||||
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
|
||||
<div className="px-4 sm:px-6 flex-1">
|
||||
<Text variant="sectionHeading"> Payment Method</Text>
|
||||
<div>
|
||||
<div className={s.fieldset}>
|
||||
<label className={s.label}>Cardholder Name</label>
|
||||
<input className={s.input} />
|
||||
</div>
|
||||
<div className="grid gap-3 grid-flow-row grid-cols-12">
|
||||
<div className={cn(s.fieldset, 'col-span-7')}>
|
||||
<label className={s.label}>Card Number</label>
|
||||
<input className={s.input} />
|
||||
<form className="h-full" onSubmit={handleSubmit}>
|
||||
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
|
||||
<div className="px-4 sm:px-6 flex-1">
|
||||
<Text variant="sectionHeading"> Payment Method</Text>
|
||||
<div>
|
||||
<div className={s.fieldset}>
|
||||
<label className={s.label}>Cardholder Name</label>
|
||||
<input name="cardHolder" className={s.input} />
|
||||
</div>
|
||||
<div className={cn(s.fieldset, 'col-span-3')}>
|
||||
<label className={s.label}>Expires</label>
|
||||
<input className={s.input} placeholder="MM/YY" />
|
||||
<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 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 className={cn(s.fieldset, 'col-span-2')}>
|
||||
<label className={s.label}>CVC</label>
|
||||
<input className={s.input} />
|
||||
<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 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>
|
||||
<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 className={s.fieldset}>
|
||||
<label className={s.label}>Company (Optional)</label>
|
||||
<input name="company" className={s.input} />
|
||||
</div>
|
||||
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||
<label className={s.label}>Last Name</label>
|
||||
<input className={s.input} />
|
||||
<div className={s.fieldset}>
|
||||
<label className={s.label}>Street and House Number</label>
|
||||
<input name="streetNumber" className={s.input} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.fieldset}>
|
||||
<label className={s.label}>Company (Optional)</label>
|
||||
<input className={s.input} />
|
||||
</div>
|
||||
<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 className={s.fieldset}>
|
||||
<label className={s.label}>
|
||||
Apartment, Suite, Etc. (Optional)
|
||||
</label>
|
||||
<input className={s.input} name="apartment" />
|
||||
</div>
|
||||
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||
<label className={s.label}>City</label>
|
||||
<input className={s.input} />
|
||||
<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 className={s.fieldset}>
|
||||
<label className={s.label}>Country/Region</label>
|
||||
<select className={s.select}>
|
||||
<option>Hong Kong</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky z-20 bottom-0 w-full right-0 left-0 py-12 bg-accent-0 border-t border-accent-2 px-6">
|
||||
<Button Component="a" width="100%" variant="ghost">
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
<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">
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -1,14 +1,15 @@
|
||||
import { FC } from 'react'
|
||||
import s from './PaymentWidget.module.css'
|
||||
import { ChevronRight, CreditCard } from '@components/icons'
|
||||
import { ChevronRight, CreditCard, Check } from '@components/icons'
|
||||
|
||||
interface ComponentProps {
|
||||
onClick?: () => any
|
||||
isValid?: boolean
|
||||
}
|
||||
|
||||
const PaymentWidget: FC<ComponentProps> = ({ onClick }) => {
|
||||
/* Shipping Address
|
||||
Only available with checkout set to true -
|
||||
const PaymentWidget: FC<ComponentProps> = ({ onClick, isValid }) => {
|
||||
/* Shipping Address
|
||||
Only available with checkout set to true -
|
||||
This means that the provider does offer checkout functionality. */
|
||||
return (
|
||||
<div onClick={onClick} className={s.root}>
|
||||
@@ -19,9 +20,7 @@ const PaymentWidget: FC<ComponentProps> = ({ onClick }) => {
|
||||
</span>
|
||||
{/* <span>VISA #### #### #### 2345</span> */}
|
||||
</div>
|
||||
<div>
|
||||
<ChevronRight />
|
||||
</div>
|
||||
<div>{isValid ? <Check /> : <ChevronRight />}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@@ -1,77 +1,117 @@
|
||||
import { FC } from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './ShippingView.module.css'
|
||||
|
||||
import Button from '@components/ui/Button'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import SidebarLayout from '@components/common/SidebarLayout'
|
||||
import 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 { 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 (
|
||||
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
|
||||
<div className="px-4 sm:px-6 flex-1">
|
||||
<h2 className="pt-1 pb-8 text-2xl font-semibold tracking-wide cursor-pointer inline-block">
|
||||
Shipping
|
||||
</h2>
|
||||
<div>
|
||||
<div className="flex flex-row my-3 items-center">
|
||||
<input className={s.radio} type="radio" />
|
||||
<span className="ml-3 text-sm">Same as billing address</span>
|
||||
</div>
|
||||
<div className="flex flex-row my-3 items-center">
|
||||
<input className={s.radio} type="radio" />
|
||||
<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} />
|
||||
<form className="h-full" onSubmit={handleSubmit}>
|
||||
<SidebarLayout handleBack={() => setSidebarView('CHECKOUT_VIEW')}>
|
||||
<div className="px-4 sm:px-6 flex-1">
|
||||
<h2 className="pt-1 pb-8 text-2xl font-semibold tracking-wide cursor-pointer inline-block">
|
||||
Shipping
|
||||
</h2>
|
||||
<div>
|
||||
<div className="flex flex-row my-3 items-center">
|
||||
<input name="type" className={s.radio} type="radio" />
|
||||
<span className="ml-3 text-sm">Same as billing address</span>
|
||||
</div>
|
||||
<div className={cn(s.fieldset, 'col-span-6')}>
|
||||
<label className={s.label}>Last Name</label>
|
||||
<input className={s.input} />
|
||||
<div className="flex flex-row my-3 items-center">
|
||||
<input name="type" className={s.radio} type="radio" />
|
||||
<span className="ml-3 text-sm">
|
||||
Use a different shipping address
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={s.fieldset}>
|
||||
<label className={s.label}>Company (Optional)</label>
|
||||
<input className={s.input} />
|
||||
</div>
|
||||
<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} />
|
||||
<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 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 className={cn(s.fieldset, 'col-span-6')}>
|
||||
<label className={s.label}>City</label>
|
||||
<input className={s.input} />
|
||||
<div className={s.fieldset}>
|
||||
<label className={s.label}>Company (Optional)</label>
|
||||
<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 className={s.fieldset}>
|
||||
<label className={s.label}>Country/Region</label>
|
||||
<select className={s.select}>
|
||||
<option>Hong Kong</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky z-20 bottom-0 w-full right-0 left-0 py-12 bg-accent-0 border-t border-accent-2 px-6">
|
||||
<Button Component="a" width="100%" variant="ghost">
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
<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">
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -1,15 +1,16 @@
|
||||
import { FC } from 'react'
|
||||
import s from './ShippingWidget.module.css'
|
||||
import { ChevronRight, MapPin } from '@components/icons'
|
||||
import { ChevronRight, MapPin, Check } from '@components/icons'
|
||||
import cn from 'classnames'
|
||||
|
||||
interface ComponentProps {
|
||||
onClick?: () => any
|
||||
isValid?: boolean
|
||||
}
|
||||
|
||||
const ShippingWidget: FC<ComponentProps> = ({ onClick }) => {
|
||||
/* Shipping Address
|
||||
Only available with checkout set to true -
|
||||
const ShippingWidget: FC<ComponentProps> = ({ onClick, isValid }) => {
|
||||
/* Shipping Address
|
||||
Only available with checkout set to true -
|
||||
This means that the provider does offer checkout functionality. */
|
||||
return (
|
||||
<div onClick={onClick} className={s.root}>
|
||||
@@ -23,9 +24,7 @@ const ShippingWidget: FC<ComponentProps> = ({ onClick }) => {
|
||||
San Franssisco, California
|
||||
</span> */}
|
||||
</div>
|
||||
<div>
|
||||
<ChevronRight />
|
||||
</div>
|
||||
<div>{isValid ? <Check /> : <ChevronRight />}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@@ -73,7 +73,7 @@ const Footer: FC<Props> = ({ className, pages }) => {
|
||||
<div className="flex items-center text-primary text-sm">
|
||||
<span className="text-primary">Created by</span>
|
||||
<a
|
||||
rel="noopener"
|
||||
rel="noopener noreferrer"
|
||||
href="https://vercel.com"
|
||||
aria-label="Vercel.com Link"
|
||||
target="_blank"
|
||||
|
@@ -24,7 +24,7 @@ const Loading = () => (
|
||||
)
|
||||
|
||||
const dynamicProps = {
|
||||
loading: () => <Loading />,
|
||||
loading: Loading,
|
||||
}
|
||||
|
||||
const SignUpView = dynamic(
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { FC, InputHTMLAttributes, useEffect, useMemo } from 'react'
|
||||
import { FC, memo, useEffect } from 'react'
|
||||
import cn from 'classnames'
|
||||
import s from './Searchbar.module.css'
|
||||
import { useRouter } from 'next/router'
|
||||
@@ -13,7 +13,7 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
||||
|
||||
useEffect(() => {
|
||||
router.prefetch('/search')
|
||||
}, [])
|
||||
}, [router])
|
||||
|
||||
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
@@ -32,32 +32,29 @@ const Searchbar: FC<Props> = ({ className, id = 'search' }) => {
|
||||
}
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
<div className={cn(s.root, className)}>
|
||||
<label className="hidden" htmlFor={id}>
|
||||
Search
|
||||
</label>
|
||||
<input
|
||||
id={id}
|
||||
className={s.input}
|
||||
placeholder="Search for products..."
|
||||
defaultValue={router.query.q}
|
||||
onKeyUp={handleKeyUp}
|
||||
/>
|
||||
<div className={s.iconContainer}>
|
||||
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="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"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
return (
|
||||
<div className={cn(s.root, className)}>
|
||||
<label className="hidden" htmlFor={id}>
|
||||
Search
|
||||
</label>
|
||||
<input
|
||||
id={id}
|
||||
className={s.input}
|
||||
placeholder="Search for products..."
|
||||
defaultValue={router.query.q}
|
||||
onKeyUp={handleKeyUp}
|
||||
/>
|
||||
<div className={s.iconContainer}>
|
||||
<svg className={s.icon} fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="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"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
),
|
||||
[]
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Searchbar
|
||||
export default memo(Searchbar)
|
||||
|
@@ -7,6 +7,7 @@ import useCustomer from '@framework/customer/use-customer'
|
||||
import { Avatar } from '@components/common'
|
||||
import { Heart, Bag } from '@components/icons'
|
||||
import { useUI } from '@components/ui/context'
|
||||
import Button from '@components/ui/Button'
|
||||
import DropdownMenu from './DropdownMenu'
|
||||
import s from './UserNav.module.css'
|
||||
|
||||
@@ -26,9 +27,11 @@ const UserNav: FC<Props> = ({ className }) => {
|
||||
<nav className={cn(s.root, className)}>
|
||||
<ul className={s.list}>
|
||||
{process.env.COMMERCE_CART_ENABLED && (
|
||||
<li className={s.item} onClick={toggleSidebar}>
|
||||
<Bag />
|
||||
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||
<li className={s.item}>
|
||||
<Button className={s.item} variant="naked" onClick={toggleSidebar} aria-label="Cart">
|
||||
<Bag />
|
||||
{itemsCount > 0 && <span className={s.bagCount}>{itemsCount}</span>}
|
||||
</Button>
|
||||
</li>
|
||||
)}
|
||||
{process.env.COMMERCE_WISHLIST_ENABLED && (
|
||||
|
@@ -7,6 +7,7 @@ import Image, { ImageProps } from 'next/image'
|
||||
import WishlistButton from '@components/wishlist/WishlistButton'
|
||||
import usePrice from '@framework/product/use-price'
|
||||
import ProductTag from '../ProductTag'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
product: Product
|
||||
@@ -23,7 +24,6 @@ const ProductCard: FC<Props> = ({
|
||||
className,
|
||||
noNameTag = false,
|
||||
variant = 'default',
|
||||
...props
|
||||
}) => {
|
||||
const { price } = usePrice({
|
||||
amount: product.price.value,
|
||||
@@ -38,7 +38,7 @@ const ProductCard: FC<Props> = ({
|
||||
)
|
||||
|
||||
return (
|
||||
<Link href={`/product/${product.slug}`} {...props}>
|
||||
<Link href={`/product/${product.slug}`}>
|
||||
<a className={rootClassName}>
|
||||
{variant === 'slim' && (
|
||||
<>
|
||||
@@ -83,7 +83,7 @@ const ProductCard: FC<Props> = ({
|
||||
<Image
|
||||
alt={product.name || 'Product Image'}
|
||||
className={s.productImage}
|
||||
src={product.images[0].url || placeholderImg}
|
||||
src={product.images[0]?.url || placeholderImg}
|
||||
height={540}
|
||||
width={540}
|
||||
quality="85"
|
||||
|
@@ -1,50 +1,52 @@
|
||||
import { memo } from 'react'
|
||||
import { Swatch } from '@components/product'
|
||||
import type { ProductOption } from '@commerce/types/product'
|
||||
import { SelectedOptions } from '../helpers'
|
||||
import React from 'react'
|
||||
|
||||
interface ProductOptionsProps {
|
||||
options: ProductOption[]
|
||||
selectedOptions: SelectedOptions
|
||||
setSelectedOptions: React.Dispatch<React.SetStateAction<SelectedOptions>>
|
||||
}
|
||||
|
||||
const ProductOptions: React.FC<ProductOptionsProps> = React.memo(
|
||||
({ options, selectedOptions, setSelectedOptions }) => {
|
||||
return (
|
||||
<div>
|
||||
{options.map((opt) => (
|
||||
<div className="pb-4" key={opt.displayName}>
|
||||
<h2 className="uppercase font-medium text-sm tracking-wide">
|
||||
{opt.displayName}
|
||||
</h2>
|
||||
<div className="flex flex-row py-4">
|
||||
{opt.values.map((v, i: number) => {
|
||||
const active = selectedOptions[opt.displayName.toLowerCase()]
|
||||
return (
|
||||
<Swatch
|
||||
key={`${opt.id}-${i}`}
|
||||
active={v.label.toLowerCase() === active}
|
||||
variant={opt.displayName}
|
||||
color={v.hexColors ? v.hexColors[0] : ''}
|
||||
label={v.label}
|
||||
onClick={() => {
|
||||
setSelectedOptions((selectedOptions) => {
|
||||
return {
|
||||
...selectedOptions,
|
||||
[opt.displayName.toLowerCase()]:
|
||||
v.label.toLowerCase(),
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
const ProductOptions: React.FC<ProductOptionsProps> = ({
|
||||
options,
|
||||
selectedOptions,
|
||||
setSelectedOptions,
|
||||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{options.map((opt) => (
|
||||
<div className="pb-4" key={opt.displayName}>
|
||||
<h2 className="uppercase font-medium text-sm tracking-wide">
|
||||
{opt.displayName}
|
||||
</h2>
|
||||
<div className="flex flex-row py-4">
|
||||
{opt.values.map((v, i: number) => {
|
||||
const active = selectedOptions[opt.displayName.toLowerCase()]
|
||||
return (
|
||||
<Swatch
|
||||
key={`${opt.id}-${i}`}
|
||||
active={v.label.toLowerCase() === active}
|
||||
variant={opt.displayName}
|
||||
color={v.hexColors ? v.hexColors[0] : ''}
|
||||
label={v.label}
|
||||
onClick={() => {
|
||||
setSelectedOptions((selectedOptions) => {
|
||||
return {
|
||||
...selectedOptions,
|
||||
[opt.displayName.toLowerCase()]: v.label.toLowerCase(),
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductOptions
|
||||
export default memo(ProductOptions)
|
||||
|
@@ -23,7 +23,7 @@ const ProductSidebar: FC<ProductSidebarProps> = ({ product, className }) => {
|
||||
|
||||
useEffect(() => {
|
||||
selectDefaultOptionFromProduct(product, setSelectedOptions)
|
||||
}, [])
|
||||
}, [product])
|
||||
|
||||
const variant = getProductVariant(product, selectedOptions)
|
||||
const addToCart = async () => {
|
||||
|
@@ -66,17 +66,13 @@ const ProductSlider: React.FC<ProductSliderProps> = ({
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
sliderContainerRef.current!.addEventListener(
|
||||
'touchstart',
|
||||
preventNavigation
|
||||
)
|
||||
const slider = sliderContainerRef.current!
|
||||
|
||||
slider.addEventListener('touchstart', preventNavigation)
|
||||
|
||||
return () => {
|
||||
if (sliderContainerRef.current) {
|
||||
sliderContainerRef.current!.removeEventListener(
|
||||
'touchstart',
|
||||
preventNavigation
|
||||
)
|
||||
if (slider) {
|
||||
slider.removeEventListener('touchstart', preventNavigation)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
@@ -1,31 +1,30 @@
|
||||
import { FC, MouseEventHandler, memo } from 'react'
|
||||
import cn from 'classnames'
|
||||
import React from 'react'
|
||||
import s from './ProductSliderControl.module.css'
|
||||
import { ArrowLeft, ArrowRight } from '@components/icons'
|
||||
|
||||
interface ProductSliderControl {
|
||||
onPrev: React.MouseEventHandler<HTMLButtonElement>
|
||||
onNext: React.MouseEventHandler<HTMLButtonElement>
|
||||
onPrev: MouseEventHandler<HTMLButtonElement>
|
||||
onNext: MouseEventHandler<HTMLButtonElement>
|
||||
}
|
||||
|
||||
const ProductSliderControl: React.FC<ProductSliderControl> = React.memo(
|
||||
({ onPrev, onNext }) => (
|
||||
<div className={s.control}>
|
||||
<button
|
||||
className={cn(s.leftControl)}
|
||||
onClick={onPrev}
|
||||
aria-label="Previous Product Image"
|
||||
>
|
||||
<ArrowLeft />
|
||||
</button>
|
||||
<button
|
||||
className={cn(s.rightControl)}
|
||||
onClick={onNext}
|
||||
aria-label="Next Product Image"
|
||||
>
|
||||
<ArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
const ProductSliderControl: FC<ProductSliderControl> = ({ onPrev, onNext }) => (
|
||||
<div className={s.control}>
|
||||
<button
|
||||
className={cn(s.leftControl)}
|
||||
onClick={onPrev}
|
||||
aria-label="Previous Product Image"
|
||||
>
|
||||
<ArrowLeft />
|
||||
</button>
|
||||
<button
|
||||
className={cn(s.rightControl)}
|
||||
onClick={onNext}
|
||||
aria-label="Next Product Image"
|
||||
>
|
||||
<ArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
export default ProductSliderControl
|
||||
|
||||
export default memo(ProductSliderControl)
|
||||
|
@@ -7,19 +7,19 @@ import { useRouter } from 'next/router'
|
||||
import { Layout } from '@components/common'
|
||||
import { ProductCard } from '@components/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 getSlug from '@lib/get-slug'
|
||||
import rangeMap from '@lib/range-map'
|
||||
|
||||
const SORT = Object.entries({
|
||||
const SORT = {
|
||||
'trending-desc': 'Trending',
|
||||
'latest-desc': 'Latest arrivals',
|
||||
'price-asc': 'Price: Low to high',
|
||||
'price-desc': 'Price: High to low',
|
||||
})
|
||||
}
|
||||
|
||||
import {
|
||||
filterQuery,
|
||||
@@ -351,7 +351,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
|
||||
aria-haspopup="true"
|
||||
aria-expanded="true"
|
||||
>
|
||||
{sort ? `Sort: ${sort}` : 'Relevance'}
|
||||
{sort ? SORT[sort as keyof typeof SORT] : 'Relevance'}
|
||||
<svg
|
||||
className="-mr-1 ml-2 h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -398,7 +398,7 @@ export default function Search({ categories, brands }: SearchPropsType) {
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
{SORT.map(([key, text]) => (
|
||||
{Object.entries(SORT).map(([key, text]) => (
|
||||
<li
|
||||
key={key}
|
||||
className={cn(
|
||||
|
@@ -35,6 +35,15 @@
|
||||
@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:hover {
|
||||
@apply text-accent-4 border-accent-2 bg-accent-1 cursor-not-allowed;
|
||||
|
@@ -12,7 +12,7 @@ import { LoadingDots } from '@components/ui'
|
||||
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
href?: string
|
||||
className?: string
|
||||
variant?: 'flat' | 'slim' | 'ghost'
|
||||
variant?: 'flat' | 'slim' | 'ghost' | 'naked'
|
||||
active?: boolean
|
||||
type?: 'submit' | 'reset' | 'button'
|
||||
Component?: string | JSXElementConstructor<any>
|
||||
@@ -41,6 +41,7 @@ const Button: React.FC<ButtonProps> = forwardRef((props, buttonRef) => {
|
||||
{
|
||||
[s.ghost]: variant === 'ghost',
|
||||
[s.slim]: variant === 'slim',
|
||||
[s.naked]: variant === 'naked',
|
||||
[s.loading]: loading,
|
||||
[s.disabled]: disabled,
|
||||
},
|
||||
|
@@ -27,13 +27,15 @@ const Modal: FC<ModalProps> = ({ children, onClose }) => {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
|
||||
const modal = ref.current
|
||||
|
||||
if (modal) {
|
||||
disableBodyScroll(modal, { reserveScrollBarGap: true })
|
||||
window.addEventListener('keydown', handleKey)
|
||||
}
|
||||
return () => {
|
||||
if (ref && ref.current) {
|
||||
enableBodyScroll(ref.current)
|
||||
if (modal) {
|
||||
enableBodyScroll(modal)
|
||||
}
|
||||
clearAllBodyScrollLocks()
|
||||
window.removeEventListener('keydown', handleKey)
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { FC } from 'react'
|
||||
import { FC, memo } from 'react'
|
||||
import rangeMap from '@lib/range-map'
|
||||
import { Star } from '@components/icons'
|
||||
import cn from 'classnames'
|
||||
@@ -7,21 +7,19 @@ export interface RatingProps {
|
||||
value: number
|
||||
}
|
||||
|
||||
const Quantity: React.FC<RatingProps> = React.memo(({ value = 5 }) => {
|
||||
return (
|
||||
<div className="flex flex-row py-6 text-accent-9">
|
||||
{rangeMap(5, (i) => (
|
||||
<span
|
||||
key={`star_${i}`}
|
||||
className={cn('inline-block ml-1 ', {
|
||||
'text-accent-5': i >= Math.floor(value),
|
||||
})}
|
||||
>
|
||||
<Star />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
const Quantity: FC<RatingProps> = ({ value = 5 }) => (
|
||||
<div className="flex flex-row py-6 text-accent-9">
|
||||
{rangeMap(5, (i) => (
|
||||
<span
|
||||
key={`star_${i}`}
|
||||
className={cn('inline-block ml-1 ', {
|
||||
'text-accent-5': i >= Math.floor(value),
|
||||
})}
|
||||
>
|
||||
<Star />
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Quantity
|
||||
export default memo(Quantity)
|
||||
|
@@ -1,5 +1,5 @@
|
||||
.root {
|
||||
@apply fixed inset-0 h-full z-50 box-border;
|
||||
@apply fixed inset-0 h-full z-50 box-border outline-none;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
|
@@ -13,27 +13,44 @@ interface SidebarProps {
|
||||
}
|
||||
|
||||
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(() => {
|
||||
if (ref.current) {
|
||||
disableBodyScroll(ref.current, { reserveScrollBarGap: true })
|
||||
if (sidebarRef.current) {
|
||||
sidebarRef.current.focus()
|
||||
}
|
||||
|
||||
const contentElement = contentRef.current
|
||||
|
||||
if (contentElement) {
|
||||
disableBodyScroll(contentElement, { reserveScrollBarGap: true })
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (ref && ref.current) {
|
||||
enableBodyScroll(ref.current)
|
||||
}
|
||||
if (contentElement) enableBodyScroll(contentElement)
|
||||
clearAllBodyScrollLocks()
|
||||
}
|
||||
}, [])
|
||||
|
||||
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={s.backdrop} onClick={onClose} />
|
||||
<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={s.sidebar} ref={ref}>
|
||||
<div className={s.sidebar} ref={contentRef}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -2,13 +2,13 @@ import { GetAPISchema, createEndpoint } from '@commerce/api'
|
||||
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
|
||||
import type { CheckoutSchema } from '../../../types/checkout'
|
||||
import type { BigcommerceAPI } from '../..'
|
||||
import checkout from './checkout'
|
||||
import submitCheckout from './submit-checkout'
|
||||
|
||||
export type CheckoutAPI = GetAPISchema<BigcommerceAPI, CheckoutSchema>
|
||||
|
||||
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||
|
||||
export const handlers: CheckoutEndpoint['handlers'] = { checkout }
|
||||
export const handlers: CheckoutEndpoint['handlers'] = { submitCheckout }
|
||||
|
||||
const checkoutApi = createEndpoint<CheckoutAPI>({
|
||||
handler: checkoutEndpoint,
|
||||
|
@@ -1,30 +1,58 @@
|
||||
import type { CheckoutEndpoint } from '.'
|
||||
import getCustomerId from '../../utils/get-customer-id'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { uuid } from 'uuidv4'
|
||||
|
||||
const fullCheckout = true
|
||||
|
||||
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
|
||||
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
|
||||
req,
|
||||
res,
|
||||
config,
|
||||
}) => {
|
||||
const { cookies } = req
|
||||
const cartId = cookies[config.cartCookie]
|
||||
|
||||
const customerToken = cookies[config.customerCookie]
|
||||
if (!cartId) {
|
||||
res.redirect('/cart')
|
||||
return
|
||||
}
|
||||
|
||||
const { data } = await config.storeApiFetch(
|
||||
`/v3/carts/${cartId}/redirect_urls`,
|
||||
{
|
||||
method: 'POST',
|
||||
}
|
||||
)
|
||||
const customerId =
|
||||
customerToken && (await getCustomerId({ customerToken, config }))
|
||||
|
||||
if (fullCheckout) {
|
||||
res.redirect(data.checkout_url)
|
||||
return
|
||||
//if there is a customer create a jwt token
|
||||
if (!customerId) {
|
||||
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!
|
||||
@@ -59,4 +87,4 @@ const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
|
||||
res.end()
|
||||
}
|
||||
|
||||
export default checkout
|
||||
export default submitCheckout
|
1
framework/bigcommerce/api/endpoints/customer/address.ts
Normal file
1
framework/bigcommerce/api/endpoints/customer/address.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
framework/bigcommerce/api/endpoints/customer/card.ts
Normal file
1
framework/bigcommerce/api/endpoints/customer/card.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@@ -1,6 +1,6 @@
|
||||
import getCustomerWishlist from '../../operations/get-customer-wishlist'
|
||||
import { parseWishlistItem } from '../../utils/parse-item'
|
||||
import getCustomerId from './utils/get-customer-id'
|
||||
import getCustomerId from '../../utils/get-customer-id'
|
||||
import type { WishlistEndpoint } from '.'
|
||||
|
||||
// Return wishlist info
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { Wishlist } from '../../../types/wishlist'
|
||||
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'
|
||||
|
||||
// Return wishlist info
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import type { Wishlist } from '../../../types/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 '.'
|
||||
|
||||
// Return wishlist info
|
||||
|
@@ -32,6 +32,9 @@ export interface BigcommerceConfig extends CommerceAPIConfig {
|
||||
storeApiToken: string
|
||||
storeApiClientId: string
|
||||
storeChannelId?: string
|
||||
storeUrl?: string
|
||||
storeApiClientSecret?: string
|
||||
storeHash?:string
|
||||
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_CLIENT_ID = process.env.BIGCOMMERCE_STORE_API_CLIENT_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) {
|
||||
throw new Error(
|
||||
@@ -75,6 +81,9 @@ const config: BigcommerceConfig = {
|
||||
storeApiToken: STORE_API_TOKEN,
|
||||
storeApiClientId: STORE_API_CLIENT_ID,
|
||||
storeChannelId: STORE_CHANNEL_ID,
|
||||
storeUrl:STORE_URL,
|
||||
storeApiClientSecret:CLIENT_SECRET,
|
||||
storeHash: STOREFRONT_HASH,
|
||||
storeApiFetch: createFetchStoreApi(() => getCommerceApi().getConfig()),
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import type { GetCustomerIdQuery } from '../../../../schema'
|
||||
import type { BigcommerceConfig } from '../../..'
|
||||
import type { GetCustomerIdQuery } from '../../schema'
|
||||
import type { BigcommerceConfig } from '../'
|
||||
|
||||
export const getCustomerIdQuery = /* GraphQL */ `
|
||||
query getCustomerId {
|
@@ -10,7 +10,7 @@ type BCCartItemBody = {
|
||||
product_id: number
|
||||
variant_id: number
|
||||
quantity?: number
|
||||
option_selections?: OptionSelections
|
||||
option_selections?: OptionSelections[]
|
||||
}
|
||||
|
||||
export const parseWishlistItem = (
|
||||
|
@@ -16,7 +16,7 @@ export const handler: MutationHook<LoginHook> = {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to login',
|
||||
'An email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
|
14
framework/bigcommerce/checkout/use-checkout.tsx
Normal file
14
framework/bigcommerce/checkout/use-checkout.tsx
Normal 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) => ({}),
|
||||
}
|
15
framework/bigcommerce/customer/address/use-add-item.tsx
Normal file
15
framework/bigcommerce/customer/address/use-add-item.tsx
Normal 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 () => ({}),
|
||||
}
|
15
framework/bigcommerce/customer/card/use-add-item.tsx
Normal file
15
framework/bigcommerce/customer/card/use-add-item.tsx
Normal 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 () => ({}),
|
||||
}
|
@@ -1,36 +1,9 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import {
|
||||
CommerceConfig,
|
||||
CommerceProvider as CoreCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@commerce'
|
||||
import { bigcommerceProvider } from './provider'
|
||||
import type { BigcommerceProvider } from './provider'
|
||||
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
|
||||
import { bigcommerceProvider, BigcommerceProvider } from './provider'
|
||||
|
||||
export { bigcommerceProvider }
|
||||
export type { BigcommerceProvider }
|
||||
|
||||
export const bigcommerceConfig: CommerceConfig = {
|
||||
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 CommerceProvider = getCommerceProvider(bigcommerceProvider)
|
||||
|
||||
export const useCommerce = () => useCoreCommerce<BigcommerceProvider>()
|
||||
|
@@ -10,7 +10,7 @@ function normalizeProductOption(productOption: any) {
|
||||
const {
|
||||
node: {
|
||||
entityId,
|
||||
values: { edges },
|
||||
values: { edges = [] } = {},
|
||||
...rest
|
||||
},
|
||||
} = productOption
|
||||
|
@@ -22,7 +22,7 @@ export const handler: SWRHook<SearchProductsHook> = {
|
||||
const url = new URL(options.url!, 'http://a')
|
||||
|
||||
if (search) url.searchParams.set('search', search)
|
||||
if (Number.isInteger(categoryId))
|
||||
if (Number.isInteger(Number(categoryId)))
|
||||
url.searchParams.set('categoryId', String(categoryId))
|
||||
if (Number.isInteger(brandId))
|
||||
url.searchParams.set('brandId', String(brandId))
|
||||
|
@@ -40,7 +40,7 @@ export type OptionSelections = {
|
||||
|
||||
export type CartItemBody = Core.CartItemBody & {
|
||||
productId: string // The product id is always required for BC
|
||||
optionSelections?: OptionSelections
|
||||
optionSelections?: OptionSelections[]
|
||||
}
|
||||
|
||||
export type CartTypes = {
|
||||
|
@@ -1,25 +1,39 @@
|
||||
import type { CheckoutSchema } from '../../types/checkout'
|
||||
import type { GetAPISchema } from '..'
|
||||
|
||||
import { CommerceAPIError } from '../utils/errors'
|
||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
||||
import type { GetAPISchema } from '..'
|
||||
|
||||
const checkoutEndpoint: GetAPISchema<
|
||||
any,
|
||||
CheckoutSchema
|
||||
>['endpoint']['handler'] = async (ctx) => {
|
||||
const { req, res, handlers } = ctx
|
||||
const { req, res, handlers, config } = ctx
|
||||
|
||||
if (
|
||||
!isAllowedOperation(req, res, {
|
||||
GET: handlers['checkout'],
|
||||
GET: handlers['getCheckout'],
|
||||
POST: handlers['submitCheckout'],
|
||||
})
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const { cookies } = req
|
||||
const cartId = cookies[config.cartCookie]
|
||||
|
||||
try {
|
||||
const body = null
|
||||
return await handlers['checkout']({ ...ctx, body })
|
||||
// Create checkout
|
||||
if (req.method === 'GET') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['getCheckout']?.({ ...ctx, body })
|
||||
}
|
||||
|
||||
// Create checkout
|
||||
if (req.method === 'POST') {
|
||||
const body = { ...req.body, cartId }
|
||||
return await handlers['submitCheckout']({ ...ctx, body })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
|
65
framework/commerce/api/endpoints/customer/address.ts
Normal file
65
framework/commerce/api/endpoints/customer/address.ts
Normal 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
|
65
framework/commerce/api/endpoints/customer/card.ts
Normal file
65
framework/commerce/api/endpoints/customer/card.ts
Normal 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
|
@@ -1,7 +1,8 @@
|
||||
import type { CustomerSchema } from '../../types/customer'
|
||||
import { CommerceAPIError } from '../utils/errors'
|
||||
import isAllowedOperation from '../utils/is-allowed-operation'
|
||||
import type { GetAPISchema } from '..'
|
||||
import type { CustomerSchema } from '../../../types/customer'
|
||||
import type { GetAPISchema } from '../..'
|
||||
|
||||
import { CommerceAPIError } from '../../utils/errors'
|
||||
import isAllowedOperation from '../../utils/is-allowed-operation'
|
||||
|
||||
const customerEndpoint: GetAPISchema<
|
||||
any,
|
@@ -9,6 +9,8 @@ import type { SignupSchema } from '../types/signup'
|
||||
import type { ProductsSchema } from '../types/product'
|
||||
import type { WishlistSchema } from '../types/wishlist'
|
||||
import type { CheckoutSchema } from '../types/checkout'
|
||||
import type { CustomerCardSchema } from '../types/customer/card'
|
||||
import type { CustomerAddressSchema } from '../types/customer/address'
|
||||
import {
|
||||
defaultOperations,
|
||||
OPERATIONS,
|
||||
@@ -25,6 +27,8 @@ export type APISchemas =
|
||||
| ProductsSchema
|
||||
| WishlistSchema
|
||||
| CheckoutSchema
|
||||
| CustomerCardSchema
|
||||
| CustomerAddressSchema
|
||||
|
||||
export type GetAPISchema<
|
||||
C extends CommerceAPI<any>,
|
||||
|
34
framework/commerce/checkout/use-checkout.ts
Normal file
34
framework/commerce/checkout/use-checkout.ts
Normal 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
|
23
framework/commerce/checkout/use-submit-checkout.tsx
Normal file
23
framework/commerce/checkout/use-submit-checkout.tsx
Normal 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
|
@@ -8,12 +8,12 @@ const merge = require('deepmerge')
|
||||
const prettier = require('prettier')
|
||||
|
||||
const PROVIDERS = [
|
||||
'local',
|
||||
'bigcommerce',
|
||||
'saleor',
|
||||
'shopify',
|
||||
'swell',
|
||||
'vendure',
|
||||
'local',
|
||||
]
|
||||
|
||||
function getProviderName() {
|
||||
@@ -57,11 +57,27 @@ function withCommerceConfig(nextConfig = {}) {
|
||||
|
||||
// Update paths in `tsconfig.json` to point to the selected provider
|
||||
if (config.commerce.updateTSConfig !== false) {
|
||||
const staticTsconfigPath = path.join(process.cwd(), 'tsconfig.json')
|
||||
const tsconfig = require('../../tsconfig.js')
|
||||
const tsconfigPath = path.join(process.cwd(), 'tsconfig.json')
|
||||
const tsconfig = require(tsconfigPath)
|
||||
|
||||
tsconfig.compilerOptions.paths['@framework'] = [`framework/${name}`]
|
||||
tsconfig.compilerOptions.paths['@framework/*'] = [`framework/${name}/*`]
|
||||
|
||||
// When running for production it may be useful to exclude the other providers
|
||||
// from TS checking
|
||||
if (process.env.VERCEL) {
|
||||
const exclude = tsconfig.exclude.filter(
|
||||
(item) => !item.startsWith('framework/')
|
||||
)
|
||||
|
||||
tsconfig.exclude = PROVIDERS.reduce((exclude, current) => {
|
||||
if (current !== name) exclude.push(`framework/${current}`)
|
||||
return exclude
|
||||
}, exclude)
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
staticTsconfigPath,
|
||||
tsconfigPath,
|
||||
prettier.format(JSON.stringify(tsconfig), { parser: 'json' })
|
||||
)
|
||||
}
|
||||
|
21
framework/commerce/customer/address/use-add-item.tsx
Normal file
21
framework/commerce/customer/address/use-add-item.tsx
Normal 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
|
34
framework/commerce/customer/address/use-addresses.tsx
Normal file
34
framework/commerce/customer/address/use-addresses.tsx
Normal 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
|
21
framework/commerce/customer/address/use-remove-item.tsx
Normal file
21
framework/commerce/customer/address/use-remove-item.tsx
Normal 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
|
21
framework/commerce/customer/address/use-update-item.tsx
Normal file
21
framework/commerce/customer/address/use-update-item.tsx
Normal 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
|
21
framework/commerce/customer/card/use-add-item.tsx
Normal file
21
framework/commerce/customer/card/use-add-item.tsx
Normal 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
|
34
framework/commerce/customer/card/use-cards.tsx
Normal file
34
framework/commerce/customer/card/use-cards.tsx
Normal 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
|
21
framework/commerce/customer/card/use-remove-item.tsx
Normal file
21
framework/commerce/customer/card/use-remove-item.tsx
Normal 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
|
21
framework/commerce/customer/card/use-update-item.tsx
Normal file
21
framework/commerce/customer/card/use-update-item.tsx
Normal 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
|
@@ -15,6 +15,7 @@ import type {
|
||||
Signup,
|
||||
Login,
|
||||
Logout,
|
||||
Checkout,
|
||||
} from '@commerce/types'
|
||||
|
||||
import type { Fetcher, SWRHook, MutationHook } from './utils/types'
|
||||
@@ -29,6 +30,10 @@ export type Provider = CommerceConfig & {
|
||||
useUpdateItem?: MutationHook<Cart.UpdateItemHook>
|
||||
useRemoveItem?: MutationHook<Cart.RemoveItemHook>
|
||||
}
|
||||
checkout?: {
|
||||
useCheckout?: SWRHook<Checkout.GetCheckoutHook>
|
||||
useSubmitCheckout?: MutationHook<Checkout.SubmitCheckoutHook>
|
||||
}
|
||||
wishlist?: {
|
||||
useWishlist?: SWRHook<Wishlist.GetWishlistHook>
|
||||
useAddItem?: MutationHook<Wishlist.AddItemHook>
|
||||
@@ -36,6 +41,18 @@ export type Provider = CommerceConfig & {
|
||||
}
|
||||
customer?: {
|
||||
useCustomer?: SWRHook<Customer.CustomerHook>
|
||||
card?: {
|
||||
useCards?: SWRHook<Customer.Card.GetCardsHook>
|
||||
useAddItem?: MutationHook<Customer.Card.AddItemHook>
|
||||
useUpdateItem?: MutationHook<Customer.Card.UpdateItemHook>
|
||||
useRemoveItem?: MutationHook<Customer.Card.RemoveItemHook>
|
||||
}
|
||||
address?: {
|
||||
useAddresses?: SWRHook<Customer.Address.GetAddressesHook>
|
||||
useAddItem?: MutationHook<Customer.Address.AddItemHook>
|
||||
useUpdateItem?: MutationHook<Customer.Address.UpdateItemHook>
|
||||
useRemoveItem?: MutationHook<Customer.Address.RemoveItemHook>
|
||||
}
|
||||
}
|
||||
products?: {
|
||||
useSearch?: SWRHook<Product.SearchProductsHook>
|
||||
@@ -47,51 +64,60 @@ export type Provider = CommerceConfig & {
|
||||
}
|
||||
}
|
||||
|
||||
export type CommerceProps<P extends Provider> = {
|
||||
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>
|
||||
export type CommerceConfig = {
|
||||
locale: 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,
|
||||
children,
|
||||
config,
|
||||
}: CommerceProps<P>) {
|
||||
if (!config) {
|
||||
throw new Error('CommerceProvider requires a valid config object')
|
||||
}
|
||||
|
||||
const providerRef = useRef(provider)
|
||||
// TODO: Remove the fetcherRef
|
||||
const fetcherRef = useRef(provider.fetcher)
|
||||
// Because the config is an object, if the parent re-renders this provider
|
||||
// will re-render every consumer unless we memoize the config
|
||||
// If the parent re-renders this provider will re-render every
|
||||
// consumer unless we memoize the config
|
||||
const { locale, cartCookie } = providerRef.current
|
||||
const cfg = useMemo(
|
||||
() => ({
|
||||
providerRef,
|
||||
fetcherRef,
|
||||
locale: config.locale,
|
||||
cartCookie: config.cartCookie,
|
||||
}),
|
||||
[config.locale, config.cartCookie]
|
||||
() => ({ providerRef, fetcherRef, locale, cartCookie }),
|
||||
[locale, cartCookie]
|
||||
)
|
||||
|
||||
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>() {
|
||||
return useContext(Commerce) as CommerceContextValue<P>
|
||||
}
|
||||
|
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
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:
|
||||
|
@@ -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: {
|
||||
options: {}
|
||||
handlers: {
|
||||
checkout: {
|
||||
data: null
|
||||
}
|
||||
}
|
||||
handlers: CheckoutHandlers<T>
|
||||
}
|
||||
}
|
||||
|
93
framework/commerce/types/customer/address.ts
Normal file
93
framework/commerce/types/customer/address.ts
Normal 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>
|
||||
}
|
||||
}
|
96
framework/commerce/types/customer/card.ts
Normal file
96
framework/commerce/types/customer/card.ts
Normal 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>
|
||||
}
|
||||
}
|
@@ -1,3 +1,6 @@
|
||||
export * as Card from "./card"
|
||||
export * as Address from "./address"
|
||||
|
||||
// TODO: define this type
|
||||
export type Customer = any
|
||||
|
@@ -87,6 +87,8 @@ export type HookSchemaBase = {
|
||||
export type SWRHookSchemaBase = HookSchemaBase & {
|
||||
// Custom state added to the response object of SWR
|
||||
swrState?: {}
|
||||
// Instances of MutationSchemaBase that the hook returns for better DX
|
||||
mutations?: Record<string, ReturnType<MutationHook<any>['useHook']>>
|
||||
}
|
||||
|
||||
export type MutationSchemaBase = HookSchemaBase & {
|
||||
@@ -102,7 +104,7 @@ export type SWRHook<H extends SWRHookSchemaBase> = {
|
||||
context: SWRHookContext<H>
|
||||
): HookFunction<
|
||||
H['input'] & { swrOptions?: SwrOptions<H['data'], H['fetcherInput']> },
|
||||
ResponseState<H['data']> & H['swrState']
|
||||
ResponseState<H['data']> & H['swrState'] & H['mutations']
|
||||
>
|
||||
fetchOptions: HookFetcherOptions
|
||||
fetcher?: HookFetcherFn<H>
|
||||
|
1
framework/local/api/endpoints/customer/address.ts
Normal file
1
framework/local/api/endpoints/customer/address.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
framework/local/api/endpoints/customer/card.ts
Normal file
1
framework/local/api/endpoints/customer/card.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
14
framework/local/checkout/use-checkout.tsx
Normal file
14
framework/local/checkout/use-checkout.tsx
Normal 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) => ({}),
|
||||
}
|
15
framework/local/customer/address/use-add-item.tsx
Normal file
15
framework/local/customer/address/use-add-item.tsx
Normal 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 () => ({}),
|
||||
}
|
15
framework/local/customer/card/use-add-item.tsx
Normal file
15
framework/local/customer/card/use-add-item.tsx
Normal 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 () => ({}),
|
||||
}
|
@@ -1,32 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
import { localProvider } from './provider'
|
||||
import {
|
||||
CommerceConfig,
|
||||
CommerceProvider as CoreCommerceProvider,
|
||||
useCommerce as useCoreCommerce,
|
||||
} from '@commerce'
|
||||
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
|
||||
import { localProvider, LocalProvider } from './provider'
|
||||
|
||||
export const localConfig: CommerceConfig = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'session',
|
||||
}
|
||||
export { localProvider }
|
||||
export type { LocalProvider }
|
||||
|
||||
export function CommerceProvider({
|
||||
children,
|
||||
...config
|
||||
}: {
|
||||
children?: ReactNode
|
||||
locale: string
|
||||
} & Partial<CommerceConfig>) {
|
||||
return (
|
||||
<CoreCommerceProvider
|
||||
provider={localProvider}
|
||||
config={{ ...localConfig, ...config }}
|
||||
>
|
||||
{children}
|
||||
</CoreCommerceProvider>
|
||||
)
|
||||
}
|
||||
export const CommerceProvider = getCommerceProvider(localProvider)
|
||||
|
||||
export const useCommerce = () => useCoreCommerce()
|
||||
export const useCommerce = () => useCoreCommerce<LocalProvider>()
|
||||
|
@@ -9,7 +9,6 @@ import { handler as useLogin } from './auth/use-login'
|
||||
import { handler as useLogout } from './auth/use-logout'
|
||||
import { handler as useSignup } from './auth/use-signup'
|
||||
|
||||
export type Provider = typeof localProvider
|
||||
export const localProvider = {
|
||||
locale: 'en-us',
|
||||
cartCookie: 'session',
|
||||
@@ -19,3 +18,5 @@ export const localProvider = {
|
||||
products: { useSearch },
|
||||
auth: { useLogin, useLogout, useSignup },
|
||||
}
|
||||
|
||||
export type LocalProvider = typeof localProvider
|
||||
|
@@ -1,19 +1,22 @@
|
||||
## 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
|
||||
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
|
||||
|
||||
Our commitment to Open Source can be found [here](https://vercel.com/oss).
|
||||
|
||||
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_PROVIDER=saleor
|
||||
NEXT_PUBLIC_SALEOR_API_URL=https://vercel.saleor.cloud/graphql/
|
||||
NEXT_PUBLIC_SALEOR_CHANNEL=default-channel
|
||||
COMMERCE_IMAGE_HOST=vercel.saleor.cloud
|
||||
```
|
||||
|
@@ -6,11 +6,7 @@ export type CheckoutAPI = GetAPISchema<CommerceAPI, CheckoutSchema>
|
||||
|
||||
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||
|
||||
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
|
||||
req,
|
||||
res,
|
||||
config,
|
||||
}) => {
|
||||
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({ req, res, config }) => {
|
||||
try {
|
||||
const 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>({
|
||||
handler: checkoutEndpoint,
|
||||
|
@@ -1 +0,0 @@
|
||||
export default function (_commerce: any) {}
|
1
framework/saleor/api/endpoints/customer/address.ts
Normal file
1
framework/saleor/api/endpoints/customer/address.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
framework/saleor/api/endpoints/customer/card.ts
Normal file
1
framework/saleor/api/endpoints/customer/card.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
framework/saleor/api/endpoints/customer/index.ts
Normal file
1
framework/saleor/api/endpoints/customer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@@ -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'
|
||||
|
||||
if (!Const.API_URL) {
|
||||
@@ -27,23 +27,12 @@ const config: SaleorConfig = {
|
||||
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 type Provider = typeof provider
|
||||
|
||||
export type SaleorAPI<P extends Provider = Provider> = CommerceAPI<P>
|
||||
|
||||
export function getCommerceApi<P extends Provider>(
|
||||
customProvider: P = provider as any
|
||||
): SaleorAPI<P> {
|
||||
export function getCommerceApi<P extends Provider>(customProvider: P = provider as any): SaleorAPI<P> {
|
||||
return commerceApi(customProvider)
|
||||
}
|
||||
|
@@ -1,10 +1,6 @@
|
||||
import type { OperationContext } from '@commerce/api/operations'
|
||||
import {
|
||||
GetAllProductPathsQuery,
|
||||
GetAllProductPathsQueryVariables,
|
||||
ProductCountableEdge,
|
||||
} from '../../schema'
|
||||
import type { ShopifyConfig, Provider, SaleorConfig } from '..'
|
||||
import { ProductCountableEdge } from '../../schema'
|
||||
import type { Provider, SaleorConfig } from '..'
|
||||
|
||||
import { getAllProductsPathsQuery } from '../../utils/queries'
|
||||
import fetchAllProducts from '../utils/fetch-all-products'
|
||||
@@ -13,10 +9,7 @@ export type GetAllProductPathsResult = {
|
||||
products: Array<{ path: string }>
|
||||
}
|
||||
|
||||
export default function getAllProductPathsOperation({
|
||||
commerce,
|
||||
}: OperationContext<Provider>) {
|
||||
|
||||
export default function getAllProductPathsOperation({ commerce }: OperationContext<Provider>) {
|
||||
async function getAllProductPaths({
|
||||
query,
|
||||
config,
|
||||
@@ -24,7 +17,7 @@ export default function getAllProductPathsOperation({
|
||||
}: {
|
||||
query?: string
|
||||
config?: SaleorConfig
|
||||
variables?: any
|
||||
variables?: any
|
||||
} = {}): Promise<GetAllProductPathsResult> {
|
||||
config = commerce.getConfig(config)
|
||||
|
||||
@@ -39,7 +32,6 @@ export default function getAllProductPathsOperation({
|
||||
path: `/${slug}`,
|
||||
})),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return getAllProductPaths
|
||||
|
14
framework/saleor/checkout/use-checkout.tsx
Normal file
14
framework/saleor/checkout/use-checkout.tsx
Normal 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) => ({}),
|
||||
}
|
15
framework/saleor/customer/address/use-add-item.tsx
Normal file
15
framework/saleor/customer/address/use-add-item.tsx
Normal 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 () => ({}),
|
||||
}
|
15
framework/saleor/customer/card/use-add-item.tsx
Normal file
15
framework/saleor/customer/card/use-add-item.tsx
Normal 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 () => ({}),
|
||||
}
|
@@ -1,32 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import { CommerceConfig, CommerceProvider as CoreCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
|
||||
|
||||
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
|
||||
import { saleorProvider, SaleorProvider } from './provider'
|
||||
import * as Const from './const'
|
||||
|
||||
export { saleorProvider }
|
||||
export type { SaleorProvider }
|
||||
|
||||
export const saleorConfig: CommerceConfig = {
|
||||
locale: 'en-us',
|
||||
cartCookie: Const.CHECKOUT_ID_COOKIE,
|
||||
}
|
||||
export const CommerceProvider = getCommerceProvider(saleorProvider)
|
||||
|
||||
export type SaleorConfig = Partial<CommerceConfig>
|
||||
|
||||
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()
|
||||
export const useCommerce = () => useCoreCommerce<SaleorProvider>()
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import { CHECKOUT_ID_COOKIE } from './const'
|
||||
import { handler as useCart } from './cart/use-cart'
|
||||
import { handler as useAddItem } from './cart/use-add-item'
|
||||
import { handler as useUpdateItem } from './cart/use-update-item'
|
||||
@@ -14,8 +15,7 @@ import fetcher from './fetcher'
|
||||
|
||||
export const saleorProvider = {
|
||||
locale: 'en-us',
|
||||
cartCookie: '',
|
||||
cartCookieToken: '',
|
||||
cartCookie: CHECKOUT_ID_COOKIE,
|
||||
fetcher,
|
||||
cart: { useCart, useAddItem, useUpdateItem, useRemoveItem },
|
||||
customer: { useCustomer },
|
||||
|
@@ -2,13 +2,13 @@ import { GetAPISchema, createEndpoint } from '@commerce/api'
|
||||
import checkoutEndpoint from '@commerce/api/endpoints/checkout'
|
||||
import type { CheckoutSchema } from '../../../types/checkout'
|
||||
import type { ShopifyAPI } from '../..'
|
||||
import checkout from './checkout'
|
||||
import submitCheckout from './submit-checkout'
|
||||
|
||||
export type CheckoutAPI = GetAPISchema<ShopifyAPI, CheckoutSchema>
|
||||
|
||||
export type CheckoutEndpoint = CheckoutAPI['endpoint']
|
||||
|
||||
export const handlers: CheckoutEndpoint['handlers'] = { checkout }
|
||||
export const handlers: CheckoutEndpoint['handlers'] = { submitCheckout }
|
||||
|
||||
const checkoutApi = createEndpoint<CheckoutAPI>({
|
||||
handler: checkoutEndpoint,
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
import associateCustomerWithCheckoutMutation from '../../../utils/mutations/associate-customer-with-checkout'
|
||||
import type { CheckoutEndpoint } from '.'
|
||||
|
||||
const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
|
||||
const submitCheckout: CheckoutEndpoint['handlers']['submitCheckout'] = async ({
|
||||
req,
|
||||
res,
|
||||
config,
|
||||
@@ -35,4 +35,4 @@ const checkout: CheckoutEndpoint['handlers']['checkout'] = async ({
|
||||
}
|
||||
}
|
||||
|
||||
export default checkout
|
||||
export default submitCheckout
|
@@ -1 +0,0 @@
|
||||
export default function (_commerce: any) {}
|
1
framework/shopify/api/endpoints/customer/address.ts
Normal file
1
framework/shopify/api/endpoints/customer/address.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
framework/shopify/api/endpoints/customer/card.ts
Normal file
1
framework/shopify/api/endpoints/customer/card.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
1
framework/shopify/api/endpoints/customer/index.ts
Normal file
1
framework/shopify/api/endpoints/customer/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export default function noopApi(...args: any[]): void {}
|
@@ -22,7 +22,7 @@ export const handler: MutationHook<LoginHook> = {
|
||||
if (!(email && password)) {
|
||||
throw new CommerceError({
|
||||
message:
|
||||
'A first name, last name, email and password are required to login',
|
||||
'An email and password are required to login',
|
||||
})
|
||||
}
|
||||
|
||||
|
14
framework/shopify/checkout/use-checkout.tsx
Normal file
14
framework/shopify/checkout/use-checkout.tsx
Normal 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) => ({}),
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schema": {
|
||||
"https://${NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN}/api/2021-01/graphql.json": {
|
||||
"https://${NEXT_PUBLIC_SHOPIFY_STORE_DOMAIN}/api/2021-07/graphql.json": {
|
||||
"headers": {
|
||||
"X-Shopify-Storefront-Access-Token": "${NEXT_PUBLIC_SHOPIFY_STOREFRONT_ACCESS_TOKEN}"
|
||||
}
|
||||
|
15
framework/shopify/customer/address/use-add-item.tsx
Normal file
15
framework/shopify/customer/address/use-add-item.tsx
Normal 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 () => ({}),
|
||||
}
|
15
framework/shopify/customer/card/use-add-item.tsx
Normal file
15
framework/shopify/customer/card/use-add-item.tsx
Normal 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 () => ({}),
|
||||
}
|
@@ -1,40 +1,9 @@
|
||||
import * as React from 'react'
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
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'
|
||||
import { getCommerceProvider, useCommerce as useCoreCommerce } from '@commerce'
|
||||
import { shopifyProvider, ShopifyProvider } from './provider'
|
||||
|
||||
export { shopifyProvider }
|
||||
export type { ShopifyProvider }
|
||||
|
||||
export const shopifyConfig: CommerceConfig = {
|
||||
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 CommerceProvider = getCommerceProvider(shopifyProvider)
|
||||
|
||||
export const useCommerce = () => useCoreCommerce<ShopifyProvider>()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user