All files / src/sections/PayBuilder/components/products CheckoutNavButton.tsx

81.81% Statements 36/44
71.42% Branches 35/49
100% Functions 5/5
81.81% Lines 36/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228                                      12x         61x   61x 61x 61x       12x                                 61x 61x                 61x 61x 61x       61x   61x   61x 4x 4x                             4x           4x 4x 4x 4x                       61x         61x   61x 61x 61x       61x     61x   61x 12x                     61x       61x             61x   61x   61x         61x                                                                                                                                            
import { MAX_ALLOWED_AMOUNT } from "@constants/constants";
import { CANNOT_EXCEED_MAX_AMOUNT } from "@constants/stringConstants";
import { ModalHolder, useModal } from "@ebay/nice-modal-react";
import { useAddToCart } from "@hooks/merchant-api/cart";
import { Box, rgbToHex } from "@mui/material";
import { parseNumber } from "@sections/PayBuilder/helpers";
import { useCart } from "@sections/PayBuilder/provider/CartContext";
import { usePayBuilderContext } from "@sections/PayBuilder/provider/PayBuilderContext";
import { usePayBuilderForm } from "@sections/PayBuilder/provider/PayBuilderFormProvider";
import { getTextColorsBasedOnBackground } from "@sections/PayBuilder/utils";
import GiveButton from "@shared/Button/GiveButton";
import GiveTooltip from "@shared/Tooltip/GiveTooltip";
import { isEmpty } from "lodash";
import { CART_PREVIEW_MODAL } from "modals/modal_names";
import { useEffect } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { TFormStatusEnum } from "./types";
import * as Sentry from "@sentry/react";
 
const PARAM_KEYS = ["email"] as const;
 
function useQueryParams<T extends readonly string[]>(
  paramKeys: T,
): { [K in T[number]]: string | null } {
  const [searchParams] = useSearchParams();
 
  return paramKeys.reduce((acc, key) => {
    (acc as any)[key] = searchParams.get(key) ?? null;
    return acc;
  }, {} as { [K in T[number]]: string | null });
}
 
let count = 0;
 
export function CheckoutNavButton({
  fullWidth,
  isPeekMode,
  paybuilderContextValues,
  paybuilderFormValues,
  successCb,
  disabled = false,
}: {
  fullWidth?: boolean;
  paybuilderContextValues?: any;
  paybuilderFormValues?: any;
  successCb?: () => void;
  disabled?: boolean;
  isPeekMode?: boolean;
}) {
  const params = useQueryParams(PARAM_KEYS);
  const navigate = useNavigate();
  const {
    cartItems,
    setOutOfStockItemIds,
    outOfStockItemIds,
    addToCart,
    removeFromCart,
    subTotal,
    cartModalProps,
  } = useCart();
  const { id } = useParams();
  const { isLoading, addToCartHandler } = useAddToCart({
    showModal: false,
    newCartItems: cartItems,
  });
  const isAmountExceeded = parseNumber(subTotal) > MAX_ALLOWED_AMOUNT;
 
  const openCheckout = !!params.email;
 
  const onHandleCheckout = async () => {
    try {
      const result = await addToCartHandler();
 
      // FIXED: addToCartHandler return values:
      // - undefined: Items added successfully
      // - true: Checkout is disallowed (expired form, merchant can't process money)
      // - "error": All items failed to add (network, server error, etc.) - error already shown
      //
      // NOTE: Previously assumed array return meant "out of stock", but addToCartHandler
      // was returning array for ANY failure (network, timeout, server error, etc).
      // This incorrectly conflated different error types - setting ALL failures as
      // "out of stock" and showing cart modal even for network errors.
      //
      // Proper out-of-stock handling should happen BEFORE calling addToCartHandler
      // by checking item.in_stock === 0 in cart items and preventing the add attempt.
 
      Iif (result === true || result === "error") {
        // Checkout is disallowed or items failed to add (already handled)
        return;
      }
 
      // Items added successfully - proceed to checkout
      Eif (!result) {
        successCb?.();
        setCheckoutBottomSheetVisible(true);
        Iif (cartModalProps?.open) {
          cartModalProps.onClose();
        }
      }
    } catch (err) {
      Sentry.captureException(err);
    }
  };
 
  /* This button is used in nicemodal, where paybuilder hooks are not provided.
     For this reason we are passing default values for modal.
  */
  const builderFormValues = usePayBuilderForm();
  const {
    parsedValues,
    setCheckoutBottomSheetVisible,
    checkoutBottomSheetVisible,
  } = isEmpty(builderFormValues) ? paybuilderFormValues : builderFormValues;
 
  const { accentColor, checkoutContent } = parsedValues;
  const builderContextValues = usePayBuilderContext();
  const { displayStatus, screenStates } = isEmpty(builderContextValues)
    ? paybuilderContextValues
    : builderContextValues;
 
  const { isMobileView } = screenStates || {
    isMobileView: false,
  };
  const isCustomer = displayStatus === TFormStatusEnum.PUBLIC;
 
  useEffect(() => {
    Iif (openCheckout && count === 0) {
      count++;
      successCb?.();
 
      setCheckoutBottomSheetVisible(true);
      if (cartModalProps?.open) {
        cartModalProps.onClose();
      }
    }
  }, [openCheckout]);
 
  const { titleColor } = getTextColorsBasedOnBackground(
    rgbToHex(accentColor).toUpperCase(),
  );
 
  const sxW = fullWidth
    ? {
        minWidth: isMobileView ? "200px" : "340px",
      }
    : {
        maxWidth: isMobileView ? "150px" : "300px",
      };
  const cartModalHandler = useModal(CART_PREVIEW_MODAL);
 
  const isButtonLoading = isPeekMode || !isCustomer ? false : isLoading;
  const isDisabled =
    outOfStockItemIds.length > 0 ||
    disabled ||
    isAmountExceeded ||
    isButtonLoading;
 
  return (
    <Box
      sx={{
        position: "relative",
        display: "flex",
        justifyContent: "center",
        alignItems: "center",
        marginTop: "0 !important",
      }}
    >
      <GiveTooltip
        title={
          isAmountExceeded
            ? CANNOT_EXCEED_MAX_AMOUNT
            : "Please remove items that are out of stock before completing payment"
        }
        color="default"
        disableHoverListener={
          outOfStockItemIds.length === 0 && !isAmountExceeded
        }
      >
        <GiveButton
          label={checkoutContent}
          size="large"
          id="button-pre-checkout"
          loading={isButtonLoading}
          onClick={isCustomer && !disabled ? onHandleCheckout : undefined}
          sx={{
            ...sxW,
            height: "42px",
            background: accentColor,
            borderColor: accentColor,
            flex: 1,
            overflow: "hidden",
            whiteSpace: "nowrap",
            textOverflow: "ellipsis",
            color: titleColor,
            display: "flex",
            position: "relative",
            "&:hover span": {
              animation: "scroll-text 5s linear infinite",
            },
            "@keyframes scroll-text": {
              "0%": { transform: "translateX(0)" },
              "100%": { transform: "translateX(-100%)" },
            },
            "&:disabled": {
              color: `${titleColor} !important`,
            },
            // A disabled checkout kept full accent color and looked tappable — dim it
            // so the gated state is visible (mobile seat gate, max amount…). Customer
            // view only: the builder preview's cart is always empty, so isDisabled is
            // permanently true there and the merchant would never see the full accent.
            opacity: isCustomer && isDisabled ? 0.5 : 1,
          }}
          disabled={isDisabled}
        />
      </GiveTooltip>
 
      <ModalHolder
        modal={CART_PREVIEW_MODAL}
        handler={cartModalHandler}
        cartItems={cartItems}
        totalAmount={subTotal}
        addToCart={addToCart}
        removeFromCart={removeFromCart}
      />
    </Box>
  );
}