All files / src/components/Merchants/MerchantPreview/MerchantAgreement GiveMerchantAgreementContent.tsx

93.75% Statements 15/16
82.35% Branches 14/17
66.66% Functions 2/3
93.33% Lines 14/15

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 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254                                                          28x                                                     35x 35x 35x 35x     35x             35x       35x   35x             35x                 35x                     35x                                                 35x                                                                                                                                                               28x                                                                                            
import BankDisclosure from "@features/Merchants/MerchantSidePanel/AgreementAndSnapshot/Agreement/components/BankDisclosure";
import CardTypes from "@features/Merchants/MerchantSidePanel/AgreementAndSnapshot/Agreement/components/CardTypes";
import PCISection from "@features/Merchants/MerchantSidePanel/AgreementAndSnapshot/Agreement/components/PCISection";
import RefundPolicy from "@features/Merchants/MerchantSidePanel/AgreementAndSnapshot/Agreement/components/RefundPolicy";
import MerchantApplicationScheduleFee from "@features/Merchants/MerchantSidePanel/AgreementAndSnapshot/Agreement/components/MerchantApplicationScheduleFee";
import { GiveProviderAgreementContentProps } from "@features/Merchants/MerchantSidePanel/AgreementAndSnapshot/agreements.types";
import GiveSignAgreementSection from "@components/Merchants/CreateMerchantPanel/modals/components/MerchantAgreement/GiveSignAgreementSection";
import { StatusInfo } from "@features/Merchants/MerchantSidePanel/AgreementAndSnapshot/agreements.types";
import { Dispatch, SetStateAction } from "react";
import { TMerchantBaseContextData } from "../data.types";
import { TMerchantAgreementPayload } from "@components/Merchants/CreateMerchantPanel/hooks/TMerchantAgreementPayload";
import { useAgreementStatus } from "../modals/hooks/useAgreementStatus";
import GiveDisclaimer from "@features/Merchants/MerchantSidePanel/components/GiveDisclaimer/GiveDisclaimer";
import SnapshotCard from "@features/Merchants/MerchantSidePanel/AgreementAndSnapshot/Agreement/components/SnapshotCard";
import { GIVE_MERCHANT_AGREEMENT_MODAL } from "modals/modal_names";
import NiceModal from "@ebay/nice-modal-react";
import { useGetMerchantById } from "@hooks/enterprise-api/account/useGetMerchants";
import { useAppSelector } from "@redux/hooks";
import { selectUser } from "@redux/slices/auth/auth";
import {
  getLocalSignatureFile,
  parseMerchantAgreementPayload,
} from "./helpers";
import PublishedLegalContent from "@components/PublishedLegalContent/PublishedLegalContent";
import { Box } from "@mui/material";
import { styled } from "@theme/v2/Provider";
import { checkoutScrollbarStyles } from "@sections/PayBuilder/Checkout/helpers";
import useHandleScroll from "@features/Merchants/MerchantSidePanel/hooks/useHandleScroll";
 
const GiveMerchantAgreementContent = ({
  data,
  statusInfo,
  radioState,
  setRadioButtons,
  isOnboarding,
  addSignature,
  isSidepanel,
  openSnapShot,
  canBePending,
}: GiveProviderAgreementContentProps & {
  statusInfo?: StatusInfo;
  radioState?: {
    isPciChecked: boolean;
    isPreviousTerminationChecked: boolean;
  };
  setRadioButtons?: Dispatch<
    SetStateAction<{
      isPciChecked: boolean;
      isPreviousTerminationChecked: boolean;
    }>
  >;
  isOnboarding?: boolean; //this prop handle the border color
  isSidepanel?: boolean; //handles the border color of the signature box  - in merchant sidepanel and merchant settings it is not needed
  openSnapShot?: () => void;
  canBePending?: boolean;
}) => {
  const { data: APIData } = useGetMerchantById(undefined, false);
  const merchantId = data?.merchantInfo?.merchantID;
  const canSign = Boolean(radioState?.isPciChecked);
  const localSignature = getLocalSignatureFile(
    (data as any)?.merchantAgreement,
  );
  const { globalName } = useAppSelector(selectUser);
 
  // "Scroll to Bottom" affordance for the agreement scroll container (parity
  // with the provider agreement). This surface gates signing on the PCI
  // checkbox, not on scrolling, so `defaultHasAgreed: true` no-ops the
  // checkbox side effect the hook also drives; the button still shows/hides as
  // the bottom of the document is reached.
  const { scrollButton, containerRef, viewRef, handleScroll } = useHandleScroll(
    { defaultHasAgreed: true },
  );
 
  const rawAgreement = data?.merchantAgreement;
  const merchantAgreement: TMerchantAgreementPayload =
    parseMerchantAgreementPayload({
      rawAgreement,
      apiData: APIData,
      tcVersion: rawAgreement?.tcVersion,
      msaVersion: rawAgreement?.msaVersion || APIData?.msaVersion,
    });
  // Use agreement status hook
  const { status, renderSubtitle } = useAgreementStatus({
    data: merchantAgreement,
    APIData,
    signatureFile: localSignature,
    globalName,
    isMerchant: true,
    canBePending,
  });
 
  const handleOpenAgreementModal = () =>
    NiceModal.show(GIVE_MERCHANT_AGREEMENT_MODAL, {
      data: data,
      merchantId: merchantId,
      isMerchant: true,
    });
 
  // The merchant-specific data blocks (shown on every surface). The acquirer's
  // published agreement prose is NOT part of this — it is added only in the
  // create-merchant signing modal below, never inline on the side panels.
  const dataBlocks = (
    <>
      <BankDisclosure
        isOnboarding={isOnboarding}
        processor={
          data?.merchantInfo?.processor ||
          APIData?.processorName ||
          data?.merchantInfo?.parentDefaultProcessorName
        }
      />
      <RefundPolicy
        statusInfo={statusInfo}
        isOnboarding={isOnboarding}
        merchantID={merchantId}
        msaRefundPolicyFromParent={data?.merchantAgreement?.msaRefundPolicy}
      />
      <CardTypes />
      <MerchantApplicationScheduleFee
        isOnboarding={isOnboarding}
        defaultContext={
          data ? ({ data: data } as TMerchantBaseContextData) : undefined
        }
      />
    </>
  );
 
  return (
    <>
      <GiveDisclaimer
        disclaimerType={status as any}
        customTitle={`Merchant Agreement`}
        customDescription={renderSubtitle()}
        handleOpenAgreementModal={handleOpenAgreementModal}
      />
      {typeof openSnapShot === "function" && (
        <SnapshotCard onClick={openSnapShot} statusInfo={status} />
      )}
      {/* On the merchant agreement SIDE PANELS (the acquirer's Merchant side
          panel + the merchant's own Settings side panel — both pass
          `isSidepanel`) the acquirer's published agreement PROSE is not shown
          inline; it's read via the disclaimer's "Check Update" button
          (re-consent modal). Only the merchant data blocks flow here.
          The create-merchant SIGNING modal (`!isSidepanel`) shows the full
          prose + data blocks in an independent, bounded scroll box (with the
          "Scroll to Bottom" affordance mirroring the provider agreement).
          The disclaimer/snapshot above and the PCI + signature below stay
          outside it in either case. */}
      {isSidepanel ? (
        <Box display="flex" flexDirection="column" gap="20px">
          {dataBlocks}
        </Box>
      ) : (
        <AgreementDocumentScrollContainer
          data-testid="agreement"
          ref={containerRef}
          onScroll={handleScroll}
        >
          <Box display="flex" flexDirection="column" gap="20px">
            <PublishedLegalContent
              type="merchant_agreement"
              // Data-driven version/date for the static-agreement flag path —
              // same values the legacy fallback surfaces display.
              merchantAgreementVersion={{
                version: merchantAgreement?.tcVersion || "",
                lastUpdated: merchantAgreement?.tcVersionUpdatedAt || "",
              }}
            />
            {dataBlocks}
          </Box>
          {/* Bottom sentinel: toggles the sticky "Scroll to Bottom" button. */}
          <div ref={viewRef} />
          {scrollButton}
        </AgreementDocumentScrollContainer>
      )}
      <PCISection
        canBeHidden
        radioState={radioState}
        setRadioButtons={setRadioButtons}
        statusInfo={statusInfo}
        isSidepanel={isSidepanel}
      />
      <GiveSignAgreementSection
        addSignature={addSignature}
        data={data as any}
        merchantId={merchantId}
        status={status}
        canSign={canSign}
        isOnboarding={isOnboarding}
        file={localSignature}
        isSettings
        isEnterprise={false}
        isSidepanel={isSidepanel}
      />
    </>
  );
};
 
export default GiveMerchantAgreementContent;
 
// Independent scroll area for the merchant agreement document (published prose +
// the data-driven blocks) in the create-merchant modal, mirroring the provider
// agreement's Terms-of-Service scroll box so the document scrolls within its own
// frame. A plain block (like the provider TOSWrapper) so the sticky "Scroll to
// Bottom" button pins correctly; the blocks stack in an inner flex column. Only
// used off the side panels (which now flow the document with the panel), so it
// is always outlined.
const AgreementDocumentScrollContainer = styled(Box)(({ theme }) => ({
  padding: "20px",
  overflowY: "scroll",
  position: "relative",
  maxHeight: `calc(100vh - 420px)`,
  minHeight: "320px",
  borderRadius: "20px",
  border: `1px solid ${theme.palette.border?.primary}`,
  backgroundColor: theme.palette.background.paper,
  // Pin the "Scroll to Bottom" button to the bottom of the visible scroll area
  // (the hook styles it absolute; sticky keeps it in view while scrolling).
  // Scoped to the direct-child button so the data blocks' own buttons are
  // untouched.
  "& > button": {
    color: "white",
    bottom: "24px",
    position: "sticky",
  },
  ...checkoutScrollbarStyles,
  scrollbarWidth: "auto",
  "&::-webkit-scrollbar": {
    ...checkoutScrollbarStyles["&::-webkit-scrollbar"],
    width: "14px",
    backgroundColor: "transparent",
  },
  "&::-webkit-scrollbar-track": {
    ...checkoutScrollbarStyles["&::-webkit-scrollbar-track"],
    backgroundColor: "transparent",
    borderRadius: "20px !important",
    marginTop: "20px",
    marginBottom: "20px",
  },
  "&::-webkit-scrollbar-thumb": {
    ...checkoutScrollbarStyles["&::-webkit-scrollbar-thumb"],
    backgroundColor: theme.palette.primitive?.neutral[50],
    borderRadius: "10px",
    minHeight: "68px",
    border: "4px solid transparent",
    backgroundClip: "content-box",
    "&:hover": {
      ...(checkoutScrollbarStyles["&::-webkit-scrollbar-thumb"]?.["&:hover"] ||
        {}),
      backgroundColor: theme.palette.grey[300],
    },
  },
}));