Skip to content

Instantly share code, notes, and snippets.

@ikraamg
Created May 6, 2022 10:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ikraamg/761bd285746c42aae006182140c0a03c to your computer and use it in GitHub Desktop.
Save ikraamg/761bd285746c42aae006182140c0a03c to your computer and use it in GitHub Desktop.
diff --git a/graphql-cms/queries/catalog.js b/graphql-cms/queries/catalog.js
index 75d1f9d..db89a42 100644
--- a/graphql-cms/queries/catalog.js
+++ b/graphql-cms/queries/catalog.js
@@ -1,6 +1,7 @@
import { fetchGraphQL } from '../api'
import { COLLECTION_FRAGMENT } from 'graphql-cms/fragments'
import placeholder from 'graphql-cms/utils/placeholder'
+import { bootstrapReduxStore } from './redux'
const PAGE_SIZE = 1
@@ -82,3 +83,8 @@ export const getPaginatedCollections = async page => {
return collectionItems
}
+
+export const getCatalogWithReduxState = async ({ slug }, store) =>
+ Promise.all([getCatalog({ slug }), bootstrapReduxStore(store)]).then(
+ ([{ collections }]) => collections
+ )
diff --git a/graphql-cms/queries/index.js b/graphql-cms/queries/index.js
index 33e7ff6..dbf53f7 100644
--- a/graphql-cms/queries/index.js
+++ b/graphql-cms/queries/index.js
@@ -3,7 +3,7 @@ export { getProduct, getProducts, getProductWithReduxState } from './product'
export { getKit, getKits, getKitWithReduxState } from './kit'
export { getArticle, getArticles, getArticleWithReduxState } from './article'
export { getFaq, getFaqs } from './faq'
-export { getCatalog } from './catalog'
+export { getCatalog, getCatalogWithReduxState } from './catalog'
export { getSiteMetadata } from './metadata'
export { getNavigation } from './navigation'
export { getNotificationBar } from './notification'
diff --git a/locales/en.json b/locales/en.json
index f6f1fe9..c24d9f6 100644
--- a/locales/en.json
+++ b/locales/en.json
@@ -54,7 +54,10 @@
"text": "An email is on its way to ",
"shipping": "shipping to",
"payment": "payment method"
- }
+ },
+ "add_to_order": "Add to order",
+ "limited_offer": "Limited time offer",
+ "limited_offer_text": "Grab these additional items at {off_text} off. Offer expires in "
},
"product": {
"related_products": "More Great Products to Love",
@@ -64,6 +67,7 @@
"plural": "products",
"ingredients_list": "View Full Ingredients List",
"add_to_cart": "Add to cart",
+ "added_to_cart": "Added to cart",
"coming_soon": "Coming Soon",
"sold_out": "Out of Stock",
"notify_when_available": "Enter your email below to be notified when this product becomes available again.",
diff --git a/package.json b/package.json
index 4b6438c..55622ef 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"@stripe/terminal-js": "^0.9.0",
"babel-plugin-styled-components": "^1.10.7",
"copy-to-clipboard": "^3.3.1",
+ "date-fns": "^2.28.0",
"dotenv": "^10.0.0",
"humps": "^2.0.1",
"magic-sdk": "^8.0.0",
diff --git a/pages/order/confirmation.js b/pages/order/confirmation.js
index 6c8f53e..7ddf859 100644
--- a/pages/order/confirmation.js
+++ b/pages/order/confirmation.js
@@ -2,6 +2,17 @@ import React from 'react'
import Metadata from '~/components/Metadata'
import OrderConfirmationPage from '~/components/Order/Confirmation/Page'
import { useTranslate } from '~/hooks/actions'
+import { getCatalogWithReduxState } from 'graphql-cms/queries/catalog'
+import { wrapper as reduxWrapper } from '~/redux/store'
+
+export const getStaticProps = reduxWrapper.getStaticProps(store => async () => {
+ const catalog = await getCatalogWithReduxState({ slug: 'catalog' }, store)
+ return {
+ props: {
+ catalog
+ }
+ }
+})
/**
* This is left for backwards compatibility only.
@@ -15,13 +26,13 @@ import { useTranslate } from '~/hooks/actions'
*
* STRIPE_SUCCESS_URL_FORMAT=path
*/
-const OrderConfirmation = () => {
+const OrderConfirmation = ({ catalog }) => {
const translate = useTranslate()
return (
<>
<Metadata title={translate('confirmation.page_title')} />
- <OrderConfirmationPage />
+ <OrderConfirmationPage collections={catalog} />
</>
)
}
diff --git a/pages/order/confirmation/[number]/[id].js b/pages/order/confirmation/[number]/[id].js
index 8a4ae04..cb2f25e 100644
--- a/pages/order/confirmation/[number]/[id].js
+++ b/pages/order/confirmation/[number]/[id].js
@@ -3,8 +3,21 @@ import { useRouter } from 'next/router'
import { useTranslate } from '~/hooks/actions'
import Metadata from '~/components/Metadata'
import OrderConfirmationPage from '~/components/Order/Confirmation/Page'
+import { getCatalogWithReduxState } from 'graphql-cms/queries/catalog'
+import { wrapper as reduxWrapper } from '~/redux/store'
-const OrderConfirmation = () => {
+export const getServerSideProps = reduxWrapper.getServerSideProps(
+ store => async () => {
+ const catalog = await getCatalogWithReduxState({ slug: 'catalog' }, store)
+ return {
+ props: {
+ catalog
+ }
+ }
+ }
+)
+
+const OrderConfirmation = ({ catalog }) => {
const translate = useTranslate()
const [data, setData] = useState({})
@@ -26,6 +39,7 @@ const OrderConfirmation = () => {
<OrderConfirmationPage
orderNumber={data.number}
checkoutSessionId={data.id}
+ collections={catalog}
/>
</>
)
diff --git a/src/components/Generic/Ticker/index.jsx b/src/components/Generic/Ticker/index.jsx
new file mode 100644
index 0000000..3bbc45a
--- /dev/null
+++ b/src/components/Generic/Ticker/index.jsx
@@ -0,0 +1,16 @@
+import { useTicker } from '~/hooks/components/use-ticker'
+import { Text } from 'theme-ui'
+
+export const Ticker = ({ futureDate, styles }) => {
+ const { days, hours, minutes, seconds } = useTicker(futureDate)
+ const tickerContents = (
+ <>
+ {days > 0 && `${days} days, `}
+ {hours > 0 && `${hours} hours, `}
+ {minutes > 0 && `${minutes} minutes, `}
+ {`${seconds} seconds`}
+ </>
+ )
+
+ return <Text sx={styles}>{tickerContents}</Text>
+}
diff --git a/src/components/Order/Confirmation/Page/index.jsx b/src/components/Order/Confirmation/Page/index.jsx
index d4ad8f6..dcbe096 100644
--- a/src/components/Order/Confirmation/Page/index.jsx
+++ b/src/components/Order/Confirmation/Page/index.jsx
@@ -20,14 +20,22 @@ const OrderConfirmationDetails = dynamic(() =>
const OrderConfirmationContact = dynamic(() =>
import('~/components/Order/Confirmation/Contact')
)
+const CrossSellContainer = dynamic(() =>
+ import('~/components/Order/CrossSell/Container')
+)
-const OrderConfirmationPage = ({ orderNumber, checkoutSessionId }) => {
+const OrderConfirmationPage = ({
+ orderNumber,
+ checkoutSessionId,
+ collections
+}) => {
const { loadUser } = useUser()
const { finalizeCheckout } = useCart()
const [cart, setCart] = useState(null)
const [referralPurl, setReferralPurl] = useState(null)
const [error, setError] = useState(null)
const [isLoading, setIsLoading] = useState(true)
+ const [showCrossSell, setShowCrossSell] = useState(true)
useEffect(() => {
const finalizeCheckoutAsync = async () => {
@@ -69,6 +77,15 @@ const OrderConfirmationPage = ({ orderNumber, checkoutSessionId }) => {
>
<OrderConfirmationDetails cart={cart} />
+ {showCrossSell && (
+ <CrossSellContainer
+ cart={cart}
+ setCart={setCart}
+ setShowCrossSell={setShowCrossSell}
+ collections={collections}
+ />
+ )}
+
{referralPurl && (
<OrderConfirmationReferralPrompt purl={referralPurl} />
)}
diff --git a/src/components/Order/CrossSell/AddToCartButton/index.jsx b/src/components/Order/CrossSell/AddToCartButton/index.jsx
new file mode 100644
index 0000000..4bdc71d
--- /dev/null
+++ b/src/components/Order/CrossSell/AddToCartButton/index.jsx
@@ -0,0 +1,157 @@
+/** @jsxImportSource theme-ui */
+import { Fragment, useState, useCallback } from 'react'
+import { Button, Flex, Text, Spinner } from 'theme-ui'
+import { useTranslate } from '~/hooks/actions'
+import { toUsdCurrency } from '~/utils'
+import useVariantAvailability from '~/hooks/components/use-variant-availability'
+
+const AddToCartButton = ({
+ lineItemsAttributes,
+ setLineItemsAttributes,
+ disabled = false,
+ price,
+ quantity,
+ regularPrice,
+ sku,
+ interval = null,
+ ...props
+}) => {
+ const translate = useTranslate()
+
+ const isAddedToCart = sku =>
+ lineItemsAttributes.some(item => item.sku === sku)
+ const [addedToCart, setAddedToCart] = useState(() => isAddedToCart(sku))
+
+ const subscriptionAttributes = (sku, quantity, interval) => ({
+ sku: sku,
+ quantity: quantity,
+ subscription_line_items_attributes: [
+ {
+ interval_length: interval.length,
+ interval_units: interval.unit
+ }
+ ]
+ })
+
+ const handleClick = useCallback(() => {
+ if (disabled || isFetchingAvailability || !isAvailable) return
+
+ setLineItemsAttributes(currentState => {
+ interval
+ ? currentState.push(subscriptionAttributes(sku, quantity, interval))
+ : currentState.push({ sku: sku, quantity: quantity })
+
+ return currentState
+ })
+
+ setAddedToCart(isAddedToCart(sku))
+ }, [
+ disabled,
+ isFetchingAvailability,
+ isAvailable,
+ sku,
+ quantity,
+ interval,
+ isAddedToCart,
+ setAddedToCart,
+ setLineItemsAttributes
+ ])
+
+ const {
+ isAvailable,
+ isFetchingAvailability,
+ error: availabilityError
+ } = useVariantAvailability({
+ sku
+ })
+
+ return (
+ <>
+ <Button
+ onClick={handleClick}
+ disabled={disabled || isFetchingAvailability || !isAvailable}
+ {...props}
+ p={0}
+ type="submit"
+ sx={{
+ width: '100%',
+ display: 'block',
+ fontSize: ['13px', null, '16px']
+ }}
+ >
+ <Flex sx={{ height: '100%' }}>
+ <Flex
+ sx={{
+ alignItems: 'center',
+ flexGrow: 1,
+ height: '100%',
+ justifyContent: 'center',
+ padding: ['0 16px', null, '0 32px']
+ }}
+ >
+ {isFetchingAvailability && (
+ <Spinner data-testid="spinner" size="15" color="inherit" />
+ )}
+ {!isFetchingAvailability && (
+ <Fragment>
+ {!isAvailable
+ ? translate('product.sold_out')
+ : addedToCart
+ ? translate('product.added_to_cart')
+ : interval
+ ? translate('subscriptions.subscribe')
+ : translate('product.add_to_cart')}
+ </Fragment>
+ )}
+ </Flex>
+ {!isFetchingAvailability && (
+ <Flex
+ sx={{
+ height: '100%',
+ alignItems: 'center',
+ backgroundColor: 'accent',
+ padding: ['0 16px', null, '0 32px']
+ }}
+ >
+ {regularPrice !== price ? (
+ <Flex
+ sx={{
+ height: '100%',
+ flexDirection: 'column',
+ justifyContent: 'center',
+ padding: '10px 0'
+ }}
+ >
+ <Text
+ sx={{
+ textDecoration: 'line-through',
+ color: 'white',
+ opacity: '.7',
+ variant: 'text.formLabel',
+ lineHeight: ['10px', null, '12px'],
+ marginTop: ['4px', null, '0']
+ }}
+ >
+ {toUsdCurrency(regularPrice)}
+ </Text>
+ <Text sx={{ lineHeight: '1.5rem' }}>
+ {toUsdCurrency(price)}
+ </Text>
+ </Flex>
+ ) : (
+ <Text>{toUsdCurrency(price)}</Text>
+ )}
+ </Flex>
+ )}
+ </Flex>
+ </Button>
+ {availabilityError && (
+ <Text color="errorDark" variant="textLink" mt="1.5rem">
+ {availabilityError.toString()}
+ </Text>
+ )}
+ </>
+ )
+}
+
+export default AddToCartButton
diff --git a/src/components/Order/CrossSell/CheckoutButton/index.jsx b/src/components/Order/CrossSell/CheckoutButton/index.jsx
new file mode 100644
index 0000000..43e3ba6
--- /dev/null
+++ b/src/components/Order/CrossSell/CheckoutButton/index.jsx
@@ -0,0 +1,75 @@
+/** @jsxImportSource theme-ui */
+import { useState, useEffect, useCallback } from 'react'
+import { useCart, useTranslate } from '~/hooks/actions'
+import { Button, Text, Spinner } from 'theme-ui'
+
+const CheckoutButton = ({
+ cart,
+ lineItemsAttributes,
+ setCart,
+ setShowCrossSell
+}) => {
+ const { crossSell } = useCart()
+ const translate = useTranslate()
+ const [buttonIsLoading, setButtonIsLoading] = useState(false)
+ const [apiError, setApiError] = useState(null)
+
+ // cleanup on unmount
+ useEffect(() => {
+ return () => {
+ setApiError(null)
+ setButtonIsLoading(false)
+ }
+ }, [])
+
+ const handleClick = useCallback(async () => {
+ setButtonIsLoading(true)
+ setApiError(null)
+
+ try {
+ const response = await crossSell(cart, lineItemsAttributes)
+
+ setCart(response.data)
+ setShowCrossSell(false)
+ } catch (error) {
+ setApiError(translate('error.api.default'))
+ }
+
+ setButtonIsLoading(false)
+ }, [
+ crossSell,
+ cart,
+ lineItemsAttributes,
+ translate,
+ setCart,
+ setShowCrossSell
+ ])
+
+ return (
+ <>
+ <Button
+ onClick={handleClick}
+ sx={{
+ marginTop: '1rem',
+ width: '100%'
+ }}
+ >
+ {buttonIsLoading && (
+ <Spinner data-testid="spinner" size="15" color="inherit" />
+ )}
+
+ {!buttonIsLoading && (
+ <Text variant="link"> {translate('confirmation.add_to_order')}</Text>
+ )}
+ </Button>
+
+ {apiError && (
+ <Text color="errorDark" variant="textLink" mt="1.5rem">
+ {apiError}
+ </Text>
+ )}
+ </>
+ )
+}
+
+export default CheckoutButton
diff --git a/src/components/Order/CrossSell/Container/index.jsx b/src/components/Order/CrossSell/Container/index.jsx
new file mode 100644
index 0000000..89ffaeb
--- /dev/null
+++ b/src/components/Order/CrossSell/Container/index.jsx
@@ -0,0 +1,80 @@
+/** @jsxImportSource theme-ui */
+import { Card, Text, Grid } from 'theme-ui'
+import { useTranslate } from '~/hooks/actions'
+import { Ticker } from '~/components/Generic/Ticker'
+import CheckoutButton from '../CheckoutButton'
+import CrossSellProduct from '../CrossSellProduct'
+import { useState } from 'react'
+
+const CrossSellContainer = ({
+ cart = {},
+ setCart,
+ setShowCrossSell,
+ collections
+}) => {
+ const translate = useTranslate()
+ const { crossSellDetails } = cart
+
+ const [lineItemsAttributes, setLineItemsAttributes] = useState([])
+
+ const futureDate =
+ crossSellDetails.crossSellable && new Date(crossSellDetails.crossSellEndsAt)
+
+ // Selected first collection for demo purposes
+ const selectedCollection = collections[0]
+ const { items: products } = selectedCollection.productsCollection
+ const selectedProducts = [products.at(-1)]
+ const selectedSubscriptions = [products[0]]
+ const subscriptionsInterval = { length: 1, unit: 'month' }
+
+ return (
+ crossSellDetails.crossSellable && (
+ <Card
+ sx={{
+ width: '100%',
+ marginBottom: '1.5rem',
+ padding: ['32px 18px', '2.5rem']
+ }}
+ >
+ <Text as="p" sx={{ marginBottom: '0.5rem' }}>
+ <strong>{translate('confirmation.limited_offer')}</strong> -{' '}
+ {translate('confirmation.limited_offer_text', { off_text: '15%' })}
+ <Ticker styles={{ color: '#9F1908' }} futureDate={futureDate} />
+ </Text>
+ {selectedProducts && selectedProducts.length > 0 && (
+ <Grid
+ columns={[1, 2]}
+ gap={['0.75rem', '2.25rem']}
+ p={['0 0.5rem 2rem 0.5rem', '0 0 2.25rem 0']}
+ >
+ {selectedProducts.map(product => (
+ <CrossSellProduct
+ key={product.slug}
+ product={product}
+ setLineItemsAttributes={setLineItemsAttributes}
+ lineItemsAttributes={lineItemsAttributes}
+ />
+ ))}
+ {selectedSubscriptions.map(product => (
+ <CrossSellProduct
+ key={product.slug}
+ product={product}
+ setLineItemsAttributes={setLineItemsAttributes}
+ interval={subscriptionsInterval}
+ lineItemsAttributes={lineItemsAttributes}
+ />
+ ))}
+ </Grid>
+ )}
+ <CheckoutButton
+ cart={cart}
+ lineItemsAttributes={lineItemsAttributes}
+ setCart={setCart}
+ setShowCrossSell={setShowCrossSell}
+ ></CheckoutButton>
+ </Card>
+ )
+ )
+}
+
+export default CrossSellContainer
diff --git a/src/components/Order/CrossSell/CrossSellProduct/index.jsx b/src/components/Order/CrossSell/CrossSellProduct/index.jsx
new file mode 100644
index 0000000..81a40a3
--- /dev/null
+++ b/src/components/Order/CrossSell/CrossSellProduct/index.jsx
@@ -0,0 +1,85 @@
+/** @jsxImportSource theme-ui */
+import ResponsiveImage from '~/components/Generic/Image'
+import AddToCartButton from '../AddToCartButton'
+import { Box, Card, Flex, Heading, Text } from 'theme-ui'
+
+const CrossSellProduct = ({
+ product,
+ setLineItemsAttributes,
+ lineItemsAttributes,
+ interval = null
+}) => {
+ const { mainImage, name, shortDescription } = product
+ const { items: variants } = product.variantsCollection
+
+ // Select the first variant for demo purposes and artifically drop price by 15% which should match the set promotion.
+ const { regularPrice, size, sku } = variants[0]
+ const price = regularPrice - regularPrice * 0.15
+
+ return (
+ <Card
+ sx={{
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'left',
+ flexDirection: 'column'
+ }}
+ >
+ <Flex
+ sx={{
+ justifyContent: 'flex-start',
+ flexDirection: 'column'
+ }}
+ >
+ {mainImage && <ResponsiveImage image={mainImage} />}
+
+ <Flex
+ pt={['0.5rem', '0.5rem', '1.25rem']}
+ sx={{
+ justifyContent: 'flex-start',
+ alignItems: 'left',
+ flexDirection: 'column'
+ }}
+ >
+ <Heading
+ as="h3"
+ sx={{
+ display: 'block',
+ textTransform: 'none',
+ variant: ['text.h3', 'text.h3', 'text.h3'],
+ textAlign: 'left',
+ marginBottom: ['4px', null, '6px']
+ }}
+ >
+ {name}
+ </Heading>
+ {shortDescription}
+ </Flex>
+ </Flex>
+ <Box>
+ <Flex
+ color="primary"
+ sx={{
+ justifyContent: 'space-between',
+ paddingTop: ['6px', null, '12px'],
+ paddingBottom: ['8px', '8px', '16px']
+ }}
+ >
+ {size && <Text>{size}</Text>}
+ </Flex>
+ <AddToCartButton
+ price={price}
+ quantity={1}
+ regularPrice={regularPrice}
+ sku={sku}
+ sx={{ width: '100%' }}
+ setLineItemsAttributes={setLineItemsAttributes}
+ lineItemsAttributes={lineItemsAttributes}
+ interval={interval}
+ />
+ </Box>
+ </Card>
+ )
+}
+
+export default CrossSellProduct
diff --git a/src/hooks/actions/use-cart.js b/src/hooks/actions/use-cart.js
index 1341e0e..1cc34c2 100644
--- a/src/hooks/actions/use-cart.js
+++ b/src/hooks/actions/use-cart.js
@@ -67,6 +67,11 @@ export function useCart() {
[dispatch]
)
+ const crossSell = useMemo(
+ () => bindActionCreators(actions.crossSell, dispatch),
+ [dispatch]
+ )
+
return {
cart,
addPromoCode,
@@ -80,6 +85,7 @@ export function useCart() {
modifyGiftCards,
removeFromCart,
removePromoCode,
- subscribeProduct
+ subscribeProduct,
+ crossSell
}
}
diff --git a/src/hooks/components/use-ticker.js b/src/hooks/components/use-ticker.js
new file mode 100644
index 0000000..ffef612
--- /dev/null
+++ b/src/hooks/components/use-ticker.js
@@ -0,0 +1,30 @@
+import { useEffect, useState } from 'react'
+import isBefore from 'date-fns/isBefore'
+import intervalToDuration from 'date-fns/intervalToDuration'
+
+export const useTicker = futureDate => {
+ const [now, setNow] = useState(new Date())
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ setNow(new Date())
+ }, 1000)
+
+ return () => {
+ clearInterval(interval)
+ }
+ }, [futureDate])
+
+ const isTimeUp = isBefore(futureDate, now)
+
+ if (isTimeUp) {
+ return { days: 0, hours: 0, minutes: 0, seconds: 0, isTimeUp }
+ }
+
+ let { days, hours, minutes, seconds } = intervalToDuration({
+ start: now,
+ end: futureDate
+ })
+
+ return { days, hours, minutes, seconds, isTimeUp }
+}
diff --git a/src/redux/actions/cart/cross-sell.js b/src/redux/actions/cart/cross-sell.js
new file mode 100644
index 0000000..6910f75
--- /dev/null
+++ b/src/redux/actions/cart/cross-sell.js
@@ -0,0 +1,35 @@
+import {
+ CROSS_SELL_ERROR,
+ CROSS_SELL_SUCCESS,
+ CROSS_SELL_REQUEST
+} from '~/redux/actions/types'
+
+export const crossSellRequest = lineItemsAttributes => ({
+ type: CROSS_SELL_REQUEST,
+ data: lineItemsAttributes
+})
+
+export const crossSellSuccess = response => ({
+ type: CROSS_SELL_SUCCESS,
+ data: response
+})
+
+export const crossSellError = (error, meta = {}) => ({
+ type: CROSS_SELL_ERROR,
+ error: true,
+ meta,
+ payload: error
+})
+
+export const crossSell =
+ (cart, lineItemsAttributes) =>
+ async (dispatch, _, { api }) => {
+ try {
+ dispatch(crossSellRequest(lineItemsAttributes))
+ const response = await api.crossSell(cart, lineItemsAttributes)
+ dispatch(crossSellSuccess(response))
+ return response
+ } catch (error) {
+ dispatch(crossSellError(error))
+ }
+ }
diff --git a/src/redux/actions/cart/index.js b/src/redux/actions/cart/index.js
index a4ac1dc..30b602e 100644
--- a/src/redux/actions/cart/index.js
+++ b/src/redux/actions/cart/index.js
@@ -16,3 +16,4 @@ export * from './get-user-wallet'
export * from './update-order-addresses'
export * from './update-order-delivery'
export * from './update-order-payment'
+export * from './cross-sell'
diff --git a/src/redux/actions/types.js b/src/redux/actions/types.js
index 2fad0de..d5490f0 100644
--- a/src/redux/actions/types.js
+++ b/src/redux/actions/types.js
@@ -108,6 +108,9 @@ export const UPDATE_ORDER_PAYMENT_ERROR = 'UPDATE_ORDER_PAYMENT_ERROR'
export const SEED_NOTIFICATIONS = 'SEED_NOTIFICATIONS'
export const ADD_NOTIFICATION = 'ADD_NOTIFICATION'
export const REMOVE_NOTIFICATION = 'REMOVE_NOTIFICATION'
+export const CROSS_SELL_ERROR = 'CROSS_SELL_ERROR'
+export const CROSS_SELL_SUCCESS = 'CROSS_SELL_SUCCESS'
+export const CROSS_SELL_REQUEST = 'CROSS_SELL_REQUEST'
const status = {
Idle: 'idle',
diff --git a/src/redux/reducers/cart.js b/src/redux/reducers/cart.js
index 6ed44b4..aefb40c 100644
--- a/src/redux/reducers/cart.js
+++ b/src/redux/reducers/cart.js
@@ -40,7 +40,10 @@ import {
UPDATE_ORDER_DELIVERY_ERROR,
UPDATE_ORDER_PAYMENT_REQUEST,
UPDATE_ORDER_PAYMENT_SUCCESS,
- UPDATE_ORDER_PAYMENT_ERROR
+ UPDATE_ORDER_PAYMENT_ERROR,
+ CROSS_SELL_ERROR,
+ CROSS_SELL_SUCCESS,
+ CROSS_SELL_REQUEST
} from '~/redux/actions/types'
import initialState from '~/redux/store/initial-state'
@@ -54,6 +57,7 @@ const cart = (state = initialState.cart, action) => {
case UPDATE_ORDER_ADDRESSES_REQUEST:
case UPDATE_ORDER_DELIVERY_REQUEST:
case UPDATE_ORDER_PAYMENT_REQUEST:
+ case CROSS_SELL_REQUEST:
return {
...state,
isFetching: true,
@@ -80,6 +84,7 @@ const cart = (state = initialState.cart, action) => {
case UPDATE_ORDER_ADDRESSES_SUCCESS:
case UPDATE_ORDER_DELIVERY_SUCCESS:
case UPDATE_ORDER_PAYMENT_SUCCESS:
+ case CROSS_SELL_SUCCESS:
return {
...state,
isFetching: false,
@@ -103,6 +108,7 @@ const cart = (state = initialState.cart, action) => {
case UPDATE_ORDER_ADDRESSES_ERROR:
case UPDATE_ORDER_DELIVERY_ERROR:
case UPDATE_ORDER_PAYMENT_ERROR:
+ case CROSS_SELL_ERROR:
return {
...state,
error: action.payload,
diff --git a/src/redux/reducers/cart.spec.js b/src/redux/reducers/cart.spec.js
index 190f8a2..53c1c34 100644
--- a/src/redux/reducers/cart.spec.js
+++ b/src/redux/reducers/cart.spec.js
@@ -40,7 +40,10 @@ import {
UPDATE_ORDER_DELIVERY_ERROR,
UPDATE_ORDER_PAYMENT_REQUEST,
UPDATE_ORDER_PAYMENT_SUCCESS,
- UPDATE_ORDER_PAYMENT_ERROR
+ UPDATE_ORDER_PAYMENT_ERROR,
+ CROSS_SELL_ERROR,
+ CROSS_SELL_SUCCESS,
+ CROSS_SELL_REQUEST
} from '~/redux/actions/types'
import initialState from '~/redux/store/initial-state'
import { mockedFullCart } from '~/__mocks__/acs/cart_full'
@@ -726,4 +729,50 @@ describe('cart reducer', () => {
error
})
})
+
+ it('should handle CROSS_SELL_REQUEST', () => {
+ expect(
+ cart(
+ {},
+ {
+ type: CROSS_SELL_REQUEST
+ }
+ )
+ ).toEqual({
+ isFetching: true,
+ error: null
+ })
+ })
+
+ it('should handle CROSS_SELL_SUCCESS', () => {
+ expect(
+ cart(
+ {},
+ {
+ type: CROSS_SELL_SUCCESS,
+ data: {}
+ }
+ )
+ ).toEqual({
+ data: {},
+ isFetching: false,
+ error: null
+ })
+ })
+
+ it('should handle CROSS_SELL_ERROR', () => {
+ expect(
+ cart(
+ {},
+ {
+ type: CROSS_SELL_ERROR,
+ error: true,
+ payload: error
+ }
+ )
+ ).toEqual({
+ isFetching: false,
+ error
+ })
+ })
})
diff --git a/src/services/api/index.js b/src/services/api/index.js
index b24f5fe..1c6d4c0 100644
--- a/src/services/api/index.js
+++ b/src/services/api/index.js
@@ -386,6 +386,25 @@ class Api {
const response = await axiosClient.post(url, props, config)
return response.data
}
+
+ async crossSell(cart, lineItemsAttributes) {
+ const url = `/api/orders/${cart.number}/cross_sell`
+ const config = {
+ transformRequest: [
+ (data, headers) => {
+ headers['X-Spree-Order-Token'] = cart.token
+ return JSON.stringify(data)
+ }
+ ]
+ }
+ const payload = {
+ order: {
+ line_items_attributes: lineItemsAttributes
+ }
+ }
+ const response = await axiosClient.put(url, payload, config)
+ return response.data
+ }
}
const api = new Api()
diff --git a/src/services/api/index.test.js b/src/services/api/index.test.js
index b4f722a..5a029db 100644
--- a/src/services/api/index.test.js
+++ b/src/services/api/index.test.js
@@ -103,6 +103,28 @@ describe('api', () => {
})
})
+ describe('crossSell', () => {
+ const cart = { number: 'CART-ID', token: 'test' }
+ const lineItemsAttributes = [{ sku: 'product-4', quantity: 8 }]
+ const payload = { order: { line_items_attributes: lineItemsAttributes } }
+
+ it('puts the request to the cart endpoint', () => {
+ axios.put = jest.fn().mockImplementation(() => {
+ return {}
+ })
+
+ api.crossSell(cart, lineItemsAttributes)
+
+ expect(axios.put).toHaveBeenCalledWith(
+ `/api/orders/${cart.number}/cross_sell`,
+ payload,
+ {
+ transformRequest: expect.any(Array)
+ }
+ )
+ })
+ })
+
describe('getStates', () => {
it('calls the states endpoint', async () => {
const iso = 'US'
diff --git a/yarn.lock b/yarn.lock
index dda9b99..389878a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3519,6 +3519,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
+date-fns@^2.28.0:
+ version "2.28.0"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2"
+ integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==
+
debug@4, debug@^4.1.0:
version "4.3.1"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment