src/pages/Order/OrderRoutes.jsx

/*
 * Order Routes
 * Author: Isaiah Sanchez <isanchez@lunchbox.io>
 *
 * This component is rendered by pages/Order.
 * This component defines all of the ordering routes.
 */

import React, {useState, useEffect} from "react";
import {Switch} from "react-router-dom";
import {config, Copy, constants, Routes, helpers} from "utils";
import {Fragments, Templates} from "components";
import {ErrorBoundary} from "components/elements";
import {useMenuContext} from "components/providers/Menu/MenuContext";
import {analytics} from "hooks";
import {Cart, Checkout, Locations} from "pages";
import {
  Complete,
  ClearDiscount,
  ClearGiftCard,
  Discount,
  GiftCard,
  Guests,
  GenerateLink,
  PackingInstructions,
  Prompt as Confirm,
  ScheduleDate,
  TableNumber,
  Upsells,
  InterceptCheckoutComplete,
  BeamPersonalCommunityImpactTabs,
} from "pages/Order";
import styles from "./style.module.scss";

const {
  Routes: {RouteWithProps},
} = Fragments;

const {ItemDetails, LoginSignup, PaymentForm} = Templates;

const {orderModsAsObject} = helpers;

const animationConfig = {
  animate: "in",
  className: styles["order-route"],
  exit: "out",
  initial: "initial",
  transition: {
    type: "tween",
  },
  variants: {
    in: {
      x: 0,
    },
    initial: {
      x: "100%",
    },
    out: {
      x: "-100%",
    },
  },
};

const beamAnimationConfig = {
  animate: "in",
  className: styles["order-route"],
  exit: "out",
  initial: "initial",
  variants: {
    in: {
      y: 0,
    },
    initial: {
      y: "100%",
    },
    out: {
      y: "100%",
    },
  },
};

/**
 * FallBack component if none of the Beam widgets works
 *
 * @param props
 * @memberof Pages.Pages/Order
 */
const FallBack = (props) => {
  useEffect(() => {
    props.onComplete();
  }, []);
  return null;
};

const OrderRoutes = React.memo(
  ({
    browserLocation,
    location,
    history,
    cart,
    orderContext,
    patronContext,
    setHeader,
    onClose,
  }) => {
    // Hooks
    const [beam, setBeam] = useState(null);
    const [ticketInformation, setTicketInformation] = useState({});
    const {packingMenu, upsellableItems, fetchUpsellMenu} = useMenuContext();

    // Constants
    const hasPackingMenu = packingMenu && packingMenu.menus.array.length;
    const hasUpsells = upsellableItems.length;

    useEffect(() => {
      fetchUpsellMenu();
      return () => {
        orderContext.resetGiftCard();
      };
    }, []);

    // Render
    return (
      <Switch location={location} key={location.pathname}>
        <RouteWithProps
          exact
          path={Routes.ROOT}
          component={Cart}
          cart={cart}
          history={history}
          orderContext={orderContext}
          patronContext={patronContext}
          hasUpsells={hasUpsells}
          hasPackingMenu={hasPackingMenu}
          animation={animationConfig}
          onUpsellSuccess={() => {
            if (hasPackingMenu) {
              history.push(Routes.Cart);
            } else if (
              config[orderContext.order.diningOption].guests &&
              (!patronContext.patron.firstName ||
                !patronContext.patron.lastName)
            ) {
              history.push(Routes.GUESTS);
            }
          }}
          addUpsellToCart={(item, group) => {
            history.push({
              pathname: Routes.UPSELLS_ITEM_DETAIL(item),
              state: {group},
            });
          }}
          setHeader={setHeader}
        />
        <RouteWithProps
          path={Routes.FETCH_DISCOUNT}
          component={Discount}
          items={orderContext.items}
          orderType={orderContext.order.diningOption}
          scheduledAt={orderContext.order.scheduledAt}
          order={orderContext}
          onSuccess={(discount) => {
            orderContext.setDiscount(discount);
            history.goBack();
          }}
          setHeader={setHeader}
          animation={animationConfig}
        />
        <RouteWithProps
          path={Routes.FETCH_GIFTCARD}
          component={GiftCard}
          onSuccess={({balance, giftCardNumber}) => {
            orderContext.setGiftCard({balance, giftCardNumber});
            history.goBack();
          }}
          animation={animationConfig}
        />
        <RouteWithProps
          path={Routes.CLEAR_GIFTCARD}
          component={ClearGiftCard}
          onConfirm={() => {
            orderContext.resetGiftCard();
            history.goBack();
          }}
          onCancel={() => history.goBack()}
          animation={animationConfig}
        />
        <RouteWithProps
          path={Routes.CLEAR_DISCOUNT}
          component={ClearDiscount}
          onConfirm={() => {
            orderContext.resetDiscount();
            history.goBack();
          }}
          onCancel={() => history.replace(Routes.ROOT)}
          animation={animationConfig}
        />

        <RouteWithProps
          path={Routes.FETCH_ITEM_DETAIL}
          component={({
            match: {
              params: {index},
            },
            location: {
              state: {isGroup},
            },
          }) => {
            const orderItem = orderContext.items[index];
            const itemDetailProps = {
              ...orderItem,
              buttonText: Copy.CART_STATIC.ACCEPT_EDITS_BUTTON_TEXT,
              close: () => history.goBack(),
              id: isGroup ? orderItem.group : orderItem.item,
              isCartItem: true,
              mods: orderModsAsObject(orderItem.mods),
              onConfirm: (data) => orderContext.editItemAtIndex(data, index),
            };

            return <ItemDetails {...itemDetailProps} />;
          }}
          animation={animationConfig}
        />
        <RouteWithProps
          path={Routes.REMOVE_CART_ITEM}
          component={({
            match: {
              params: {index},
            },
          }) => {
            const onConfirm = () => {
              if (index !== undefined) {
                analytics.product.removed({});
                orderContext.removeFromOrder(Number(index));
              } else {
                orderContext.resetDiscount();
                orderContext.clearItems();
                orderContext.clearPackingItems();
              }
              history.replace(Routes.ROOT);
            };
            const text =
              index !== undefined
                ? orderContext.items[index].name
                : "All Items";
            return (
              <Confirm
                onConfirm={onConfirm}
                onCancel={() => history.replace(Routes.ROOT)}
                name={text}
              />
            );
          }}
          animation={animationConfig}
        />

        <RouteWithProps
          path={Routes.PACKING}
          component={PackingInstructions}
          onSuccess={() => {
            if (
              config[orderContext.order.diningOption].guests &&
              (!patronContext.patron.firstName ||
                !patronContext.patron.lastName)
            ) {
              history.push(Routes.GUESTS);
            } else {
              history.push(Routes.CHECK_OUT);
            }
          }}
          animation={animationConfig}
          setHeader={setHeader}
        />

        <RouteWithProps
          exact
          path={Routes.UPSELLS}
          component={Upsells}
          onSuccess={() => {
            if (hasPackingMenu) {
              history.push(Routes.PACKING);
            } else if (
              config[orderContext.order.diningOption].guests &&
              (!patronContext.patron.firstName ||
                !patronContext.patron.lastName)
            ) {
              history.push(Routes.GUESTS);
            } else {
              history.push(Routes.CHECK_OUT);
            }
          }}
          addToCart={(item, group, restaurantGroup) => {
            history.push({
              pathname: Routes.UPSELLS_ITEM_DETAIL(item),
              state: {group, restaurantGroup},
            });
          }}
          setHeader={setHeader}
          animation={animationConfig}
        />
        <RouteWithProps
          path={Routes.UPSELLS_ITEM}
          component={({
            match: {
              params: {id},
            },
            location: {
              state: {group},
            },
          }) => (
            <ItemDetails
              id={id}
              onConfirm={(item, quantity) => {
                orderContext.addToOrder(
                  {group, ...item, isGroup: false},
                  quantity,
                  true,
                );
              }}
              showQuantity
              buttonText={Copy.CART_STATIC.ADD_TO_CART_BUTTON_TEXT}
              close={() => {
                if (config?.theme?.checkout?.upsell_layout === "Type2") {
                  history.replace(Routes.ROOT);
                } else {
                  history.replace(Routes.UPSELLS);
                }
              }}
              isUpsell
            />
          )}
          animation={animationConfig}
        />

        <RouteWithProps
          path={Routes.LOCATIONS}
          component={Locations}
          order={orderContext}
          animation={animationConfig}
        />
        <RouteWithProps
          path={Routes.SET_TABLE_NUMBER}
          component={TableNumber}
          order={orderContext}
          setHeader={setHeader}
          onSuccess={(data) => {
            orderContext.setTableNumber(data);
            history.goBack();
          }}
          animation={animationConfig}
        />
        <RouteWithProps
          path={Routes.FETCH_SCHEDULE_DATE}
          component={ScheduleDate}
          order={orderContext}
          setHeader={setHeader}
          onSuccess={() => history.goBack()}
          animation={animationConfig}
        />

        <RouteWithProps
          path={Routes.LOG_IN}
          trackerPath="cart"
          component={LoginSignup}
          lastLocation={browserLocation.pathname}
          onComplete={({isEmployee}) => {
            if (isEmployee) {
              history.replace(Routes.LINK);
            } else if (config?.theme?.checkout?.upsell_layout === "Type2") {
              history.replace(Routes.ROOT);
            } else if (hasUpsells) {
              history.replace(Routes.UPSELLS);
            } else if (hasPackingMenu) {
              history.replace(Routes.UPSELLS);
            } else {
              history.replace(Routes.CHECK_OUT);
            }
          }}
          animation={animationConfig}
        />
        <RouteWithProps
          exact
          path={Routes.GUESTS}
          component={Guests}
          history={history}
          patron={patronContext.patron}
          updatePatron={patronContext.updatePatron}
          animation={animationConfig}
        />
        <RouteWithProps
          exact
          path={Routes.CHECK_OUT}
          component={Checkout}
          history={history}
          patronContext={patronContext}
          orderContext={orderContext}
          beam={beam}
          setBeam={setBeam}
          setHeader={setHeader}
          animation={beamAnimationConfig}
          cart={cart}
          onSuccess={(order) => {
            setTicketInformation(order);
            history.replace(Routes.INTERCEPT_CHECKOUT_COMPLETE);
          }}
        />
        {/* Interceptor between Checkout & Complete for Beam new user */}
        <RouteWithProps
          path={Routes.INTERCEPT_CHECKOUT_COMPLETE}
          component={InterceptCheckoutComplete}
          render={() => {
            const onComplete = () => {
              if (beam) beam?.postTransaction(beam);
              history.replace(Routes.PURCHASE_COMPLETE);
            };
            const onSkipBeamSelection = () => {
              history.replace(Routes.PURCHASE_COMPLETE);
            };
            return (
              <ErrorBoundary error={<FallBack onComplete={onComplete} />}>
                <InterceptCheckoutComplete
                  patronEmail={patronContext?.patron?.email?.value}
                  beam={beam}
                  setBeam={setBeam}
                  onComplete={onComplete}
                  onSkipBeamSelection={onSkipBeamSelection}
                  setHeader={setHeader}
                />
              </ErrorBoundary>
            );
          }}
          animation={animationConfig}
        />
        <RouteWithProps
          path={Routes.FETCH_CARDS}
          component={PaymentForm}
          order={orderContext}
          onSuccess={() => history.goBack()}
          setHeader={setHeader}
          animation={animationConfig}
        />
        <RouteWithProps
          path={Routes.PURCHASE_COMPLETE}
          component={Complete}
          beam={beam}
          ticketInformation={ticketInformation}
          order={orderContext}
          onSuccess={onClose}
          onSeeMoreImpact={() => {
            history.replace(Routes.PERSONAL_COMMUNITY_IMPACT);
          }}
          setHeader={setHeader}
          animation={animationConfig}
        />
        <RouteWithProps
          path={Routes.PERSONAL_COMMUNITY_IMPACT}
          component={BeamPersonalCommunityImpactTabs}
          beam={beam}
          onClose={onClose}
          setHeader={setHeader}
          animation={animationConfig}
        />
        <RouteWithProps
          path={Routes.LINK}
          component={GenerateLink}
          order={orderContext}
          onSuccess={onClose}
          animation={animationConfig}
        />
      </Switch>
    );
  },
);

export default OrderRoutes;