UNPKG

@navinc/base-react-components

Version:
1,305 lines (1,229 loc) 55.6 kB
import React, { useEffect, useRef, useState } from 'react' import styled, { useTheme } from 'styled-components' import propTypes from 'prop-types' import { Formik, Field, FieldArray } from 'formik' import { toPattern, toNumber } from 'vanilla-masker' import { useDebouncedCallback } from 'use-debounce' import { isEmpty, parseJSON, loadScript, isWholeNumber, defaultPasswordValidator as passwordValidator, toUsdMoney, } from '@navinc/utils' import { StandardCard } from './standard-card.js' import Input from './input.js' import Checkbox from './checkbox.js' import Select from './select.js' import Radio from './radio.js' import Button from './button.js' import Header from './header.js' import Copy from './copy.js' import Text from './text.js' import Icon from './icon.js' import CDNAsset from './cdn-asset.js' import USStatesAndTerritories from './data/us-states-and-territories.js' import isRebrand from './is-rebrand.js' import Link from './link.js' export const getCachedValues = () => Object.fromEntries( Object.entries(parseJSON(global.localStorage.getItem('nav_ppp_form_values') ?? '{}')).filter(([key, val]) => !!val) ) export const setCachedValues = (values) => { try { global.localStorage.setItem('nav_ppp_form_values', JSON.stringify(values)) } catch (err) {} } const USStates = Object.entries(USStatesAndTerritories).map(([label, value]) => ({ label, value })) export const StyledStandardCard = styled(StandardCard)` margin-bottom: 40px; width: 100%; ` export const StyledForm = styled.form` max-width: 696px; width: 100%; padding: 0 16px; @media (${({ theme }) => theme.forLargerThanPhone}) { padding: 0; } ` export const StyledHeader = styled(Header)` margin-bottom: 24px; ` export const TwoColumn = styled.div` display: flex; flex-wrap: wrap; justify-content: space-between; ${Input} { width: 100%; } ${Select} { width: 100%; } @media (${({ theme }) => theme.forLargerThanPhone}) { ${Input} { width: 48%; } ${Select} { width: 48%; } } ` export const CardHeader = styled(Copy)` margin-bottom: 16px; display: flex; flex-flow: row wrap; align-content: baseline; &::after { content: ''; display: block; margin-top: 8px; height: 4px; width: 100%; border-radius: 4px; background-color: #ecedef; } & > ${Icon} { margin-left: 4px; } ` const InlineHeader = styled(Copy)` margin-bottom: 16px; display: flex; flex-flow: row wrap; align-content: baseline; & > ${Icon} { margin-left: 4px; } ` const FlexWrapper = styled.div` display: flex; ` const AdditionalOwnersWrapper = styled.div` position: relative; display: flex; flex-wrap: wrap; padding-right: 32px; border-bottom: 2px solid #ecedef; margin-bottom: 16px; &:last-child { border-bottom: none; margin-bottom: 0; } .action-buttons { position: absolute; right: 0; top: 0; display: flex; flex-direction: column; > ${Icon} { cursor: pointer; } } ` const AdditionalOwnerField = styled.div` flex-basis: 100%; > * { width: 100%; } @media (${({ theme }) => theme.forLargerThanPhone}) { flex-basis: 50%; padding-right: 18px; &:nth-child(even) { padding-right: 0; } } ` const EmphasizedCopy = styled(Copy)` font-style: italic; margin-bottom: 16px; ` const QuestionWrapper = styled.div` margin-bottom: 40px; ` const QuestionCopy = styled(Copy)` margin-bottom: 16px; ` const RadioGroup = styled.div` display: flex; > ${Radio} { flex: 0; margin-right: 20px; } ` export const SubmitButton = styled(Button)` display: flex; width: 100%; max-width: 343px; margin: 0 auto 16px; justify-content: center; ` const AdditionalOwner = styled(Copy)` color: ${({ theme }) => theme.navPrimary}; padding-left: 8px; cursor: pointer; ` export const InfodrawerCopyWrapper = styled.div` & > ${Copy} { margin-bottom: 16px; } ` export const CreateAccountWrapper = styled.div` ${Header} { margin-bottom: 16px; } ${Input} { margin-bottom: 8px; } @media (${({ theme }) => theme.forLargerThanPhone}) { width: 70%; margin-left: auto; margin-right: auto; } ` export const StyledCDNAsset = styled(CDNAsset)` display: block; margin-left: auto; margin-right: auto; ` export const LegalCheckbox = styled(Checkbox)` width: 100%; margin: 0 auto 16px; @media (${({ theme }) => theme.forLargerThanPhone}) { width: 424px; } ` export const ErrorsMessage = styled(Copy)` color: ${({ theme }) => (isRebrand(theme) ? theme.navStatusNegative : theme.sebastianRed200)}; text-align: center; margin-bottom: 16px; ` const ModifiedCardInsight = styled.div` display: flex; padding: 20px; margin-bottom: 16px; background-color: ${({ theme }) => theme.bubbleBlue100}; ` const IconWrapper = styled.div` width: 36px; padding-right: 16px; ` export const DisclaimerText = styled(Copy)` font-style: italic; margin-bottom: 16px; text-align: center; ` export const StyledIcon = styled(Icon)` cursor: pointer; color: ${({ color, theme }) => theme[color]}; ` export const CreateAccountHeader = styled(Header)` text-align: center; ` const CalculatorSpacer = styled.div` padding-bottom: 16px; ` const StyledText = styled(Text)` display: block; ` export const SBAPaycheckProtectionProgramForm = ({ hasCalculator = false, initialValues = {}, isSubmittingForm = false, onFormSubmit = () => {}, dispatchOpenInfoDrawer = () => {}, shouldShowPasswordField = false, }) => { const theme = useTheme() const EMPTY_OWNER = { name: '', address: '', title: '', percent: '' } const FLOATING_NUMBER_REGEX = /^[\d]{0,3}\.?[\d]{0,2}$/ // https://regex101.com/r/rfpbP7/4 const debouncedSetCachedValues = useDebouncedCallback(setCachedValues, 750) const calcEl = useRef(null) const [avgMonthlyPayroll, setAvgMonthlyPayroll] = useState(null) const [loanRequestAmount, setLoanRequestAmount] = useState(null) const handleSubmit = ({ emailAddress, password, ...rest } = {}) => { const cleanedValues = { version: 2, // Increment the version each time the shape of data changes emailAddress, ...rest, } const loginInfo = shouldShowPasswordField && { emailAddress, password } onFormSubmit(cleanedValues, loginInfo) } const { avgMonthlyPayroll: initialAvgMonthlyPayroll, loanRequestAmount: initialLoanRequestAmount } = initialValues /* The lack of initial values in these fields indicates the user has not landed here from a *completed* calculator page */ const shouldShowCalculator = hasCalculator && !parseInt(initialAvgMonthlyPayroll) && !parseInt(initialLoanRequestAmount) useEffect(() => { const handler = ({ detail } = {}) => { const { payroll12Months, result } = detail setAvgMonthlyPayroll(Math.trunc(payroll12Months / 12)) setLoanRequestAmount(result) } if (hasCalculator) { loadScript('https://creditera-assets.s3-us-west-2.amazonaws.com/javascripts/nav-sba-loan-calculator.js') } const calculatorEl = calcEl.current if (calcEl.current) { calcEl.current.addEventListener('result', handler) } return () => calculatorEl?.removeEventListener?.('result', handler) }, [hasCalculator]) return ( <Formik enableReinitialize initialValues={{ primaryContact: '', emailAddress: '', primaryContactPhone: '', businessLegalName: '', businessPhone: '', address: '', city: '', state: '', zipCode: '', dba: '', businessType: '', avgMonthlyPayroll: '', loanRequestAmount: '', numberOfEmployees: '', purposePayroll: false, purposeRent: false, purposeUtilities: false, purposeOther: false, purposeOtherDesc: '', ownerName1: '', ownerTitle1: '', ownerPercent1: '', ownerAddress1: '', additionalOwners: [], question1: '', question2: '', question3: '', question4: '', question5: '', question6: '', question7: '', question8: '', password: '', termsOfService: false, ...initialValues, ...getCachedValues(), // Pre-fill the form with any cached values we can find ...(!!avgMonthlyPayroll && { avgMonthlyPayroll }), ...(!!loanRequestAmount && { loanRequestAmount }), }} onSubmit={handleSubmit} validate={({ password, ...restOfValues }) => { debouncedSetCachedValues(restOfValues) // Update the cache as things change const { primaryContact, emailAddress, primaryContactPhone, businessLegalName, businessPhone, address, city, state, zipCode, businessType, avgMonthlyPayroll, numberOfEmployees, loanRequestAmount, purposePayroll, purposeRent, purposeUtilities, purposeOther, purposeOtherDesc, ownerName1, ownerTitle1, ownerPercent1, ownerAddress1, question1, question2, question3, question4, question5, question6, question7, question8, termsOfService, additionalOwners, } = restOfValues const PHONE_REGEX = /^\d{3}-\d{3}-\d{4}$/ const REQUIRED_MESSAGE = 'Required' const PERCENT_MESSAGE = 'Percent must be between 1 and 100' const PHONE_MESSAGE = 'Phone number is invalid' const additionalOwnersErrors = additionalOwners.map(({ name, address, title, percent }) => ({ ...(!name && { name: [REQUIRED_MESSAGE] }), ...(!address && { address: [REQUIRED_MESSAGE] }), ...(!title && { title: [REQUIRED_MESSAGE] }), ...(!percent && { percent: [REQUIRED_MESSAGE] }), ...((percent < 1 || percent > 100) && { percent: [PERCENT_MESSAGE] }), })) const additionalOwnersPercentSum = additionalOwners.reduce((sum, { percent }) => { const parsed = parseFloat(percent) if (!Number.isNaN(parsed)) return sum + parsed return sum }, 0) const totalPercent = parseFloat(ownerPercent1 || 0) + additionalOwnersPercentSum return { ...(!primaryContact && { primaryContact: [REQUIRED_MESSAGE], }), ...(primaryContact.length > 121 && { primaryContact: ['Primary Contact must be 121 characters or less'], }), ...(!emailAddress.match(/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/) && { emailAddress: ['Enter a valid email'], }), ...(!primaryContactPhone.match(PHONE_REGEX) && { primaryContactPhone: [PHONE_MESSAGE] }), ...(!businessLegalName && { businessLegalName: [REQUIRED_MESSAGE] }), ...(businessLegalName.length > 255 && { businessLegalName: ['Business Name must be 255 characters or less'], }), ...(!businessPhone.match(PHONE_REGEX) && { businessPhone: [PHONE_MESSAGE] }), ...(!address && { address: [REQUIRED_MESSAGE] }), ...(!city && { city: [REQUIRED_MESSAGE] }), ...(!state && { state: [REQUIRED_MESSAGE] }), ...(!zipCode.match(/^\d{5}$/) && { zipCode: ['We need your 5-digit ZIP Code'] }), ...(!businessType && { businessType: [REQUIRED_MESSAGE] }), ...(!avgMonthlyPayroll && { avgMonthlyPayroll: [REQUIRED_MESSAGE] }), ...(!numberOfEmployees && { numberOfEmployees: [REQUIRED_MESSAGE] }), ...(loanRequestAmount.length < 5 && { loanRequestAmount: ['Minimum loan amount $100'] }), ...(!purposePayroll && !purposeRent && !purposeUtilities && !purposeOther && { purposeOther: ['Must select one or more purpose for the loan'], }), ...(purposeOther && !purposeOtherDesc && { purposeOtherDesc: [REQUIRED_MESSAGE] }), ...(!ownerName1 && { ownerName1: [REQUIRED_MESSAGE] }), ...(ownerName1.length > 121 && { ownerName1: ['Owner name must be 121 characters or less'] }), ...(!ownerTitle1 && { ownerTitle1: [REQUIRED_MESSAGE] }), ...(ownerTitle1.length > 128 && { ownerTitle1: ['Title must be 128 characters or less'] }), ...(!(ownerPercent1 > 0 && ownerPercent1 <= 100) && { ownerPercent1: [PERCENT_MESSAGE] }), ...(!ownerAddress1 && { ownerAddress1: [REQUIRED_MESSAGE] }), ...(!question1 && { question1: [REQUIRED_MESSAGE] }), ...(!question2 && { question2: [REQUIRED_MESSAGE] }), ...(!question3 && { question3: [REQUIRED_MESSAGE] }), ...(!question4 && { question4: [REQUIRED_MESSAGE] }), ...(!question5 && { question5: [REQUIRED_MESSAGE] }), ...(!question6 && { question6: [REQUIRED_MESSAGE] }), ...(!question7 && { question7: [REQUIRED_MESSAGE] }), ...(!question8 && { question8: [REQUIRED_MESSAGE] }), ...(!termsOfService && { termsOfService: [REQUIRED_MESSAGE] }), ...(additionalOwnersErrors.some((error) => !isEmpty(error)) && { additionalOwners: additionalOwnersErrors }), ...(shouldShowPasswordField && passwordValidator(password).length && { password: passwordValidator(password) }), ...(totalPercent !== 100 && { totalPercent: [ `Total ownership must equal 100%, not ${ isWholeNumber(totalPercent) ? totalPercent : totalPercent.toFixed(2) }%.`, ], }), } }} > {({ values, errors, touched, handleChange, handleBlur, handleFocus, handleSubmit, submitCount }) => ( <StyledForm data-testid="paycheck-protection-program:form" onSubmit={handleSubmit} noValidate> <StyledHeader size="xl">Get connected to PPP funding options</StyledHeader> <StyledStandardCard> <CardHeader bold> Primary Contact Information{' '} <StyledIcon data-testid="ppp-form:info-drawer:primary-contact" name="actions/circle-faq" onClick={() => dispatchOpenInfoDrawer({ children: ( <InfodrawerCopyWrapper> <Copy> This person will be the primary contact for the application and will be emailed regarding this application. </Copy> </InfodrawerCopyWrapper> ), title: 'Primary contact information', }) } /> </CardHeader> <Input hasSpaceForErrors name="primaryContact" label="Primary Contact Name" value={values.primaryContact} onBlur={handleBlur} onChange={handleChange} isInvalid={touched.primaryContact && !!errors.primaryContact} errors={touched.primaryContact && errors.primaryContact} data-testid="ppp-form:input:primary-contact" required /> <TwoColumn> <Input hasSpaceForErrors type="email" name="emailAddress" label="Email Address" value={values.emailAddress} onBlur={handleBlur} onChange={handleChange} isInvalid={touched.emailAddress && !!errors.emailAddress} errors={touched.emailAddress && errors.emailAddress} data-testid="ppp-form:input:primary-email-address" required /> <Input hasSpaceForErrors name="primaryContactPhone" label="Primary Contact Phone Number" value={values.primaryContactPhone && toPattern(values.primaryContactPhone, '999-999-9999')} onBlur={handleBlur} onChange={(event) => { event.target.value = toPattern(event.target.value, '999-999-9999') handleChange(event) }} isInvalid={touched.primaryContactPhone && !!errors.primaryContactPhone} errors={touched.primaryContactPhone && errors.primaryContactPhone} data-testid="ppp-form:input:primary-contact-phone" required /> </TwoColumn> </StyledStandardCard> <StyledStandardCard> <CardHeader bold> Business Information{' '} <StyledIcon data-testid="ppp-form:info-drawer:business-information" name="actions/circle-faq" onClick={() => dispatchOpenInfoDrawer({ children: ( <InfodrawerCopyWrapper data-testid="ppp-form:info-drawer:dba"> <Copy> Information about the business applying for funds through the Paycheck Protection Program. </Copy> <Copy> <Text $bold>Doing Business As (DBA):</Text> A company is said to be "doing business as" when the name under which they operate their business differs from its legal, registered name. </Copy> <Copy> <Text $bold>Tradename:</Text> A trade name, trading name, or business name is a name used by companies that don't want to operate under their registered name. </Copy> </InfodrawerCopyWrapper> ), title: 'Business information', }) } /> </CardHeader> <Input hasSpaceForErrors name="businessLegalName" label="Business Legal Name" type="text" value={values.businessLegalName} onBlur={handleBlur} onChange={handleChange} isInvalid={touched.businessLegalName && !!errors.businessLegalName} errors={touched.businessLegalName && errors.businessLegalName} data-testid="ppp-form:input:business-legal-name" required /> <Input hasSpaceForErrors name="businessPhone" label="Business Phone Number" type="text" value={values.businessPhone && toPattern(values.businessPhone, '999-999-9999')} onBlur={handleBlur} onChange={(event) => { event.target.value = toPattern(event.target.value, '999-999-9999') handleChange(event) }} isInvalid={touched.businessPhone && !!errors.businessPhone} errors={touched.businessPhone && errors.businessPhone} data-testid="ppp-form:input:business-phone" required /> <Input errors={touched.address && errors.address} hasSpaceForErrors isInvalid={touched.address && errors.address} label="Business Address" name="address" onBlur={handleBlur} onChange={handleChange} value={values.address} data-testid="ppp-form:input:business-address" required /> <Input errors={touched.city && [errors.city]} hasSpaceForErrors isInvalid={touched.city && errors.city} label="City" masktype="city" name="city" onBlur={handleBlur} onChange={handleChange} value={values.city} data-testid="ppp-form:input:business-city" required /> <TwoColumn> <Select errors={touched.state && [errors.state]} hasSpaceForErrors isInvalid={touched.state && errors.state} label="State" name="state" type="text" onBlur={handleBlur} onChange={handleChange} options={USStates} value={values.state} data-testid="ppp-form:input:business-state" required /> <Input errors={touched.zipCode && errors.zipCode} hasSpaceForErrors isInvalid={touched.zipCode && errors.zipCode} label="ZIP Code" name="zipCode" onBlur={handleBlur} onChange={handleChange} value={toPattern(values.zipCode, '99999-9999')} data-testid="ppp-form:input:business-zip" required /> </TwoColumn> <Input errors={touched.dba && errors.dba} hasSpaceForErrors isInvalid={touched.dba && errors.dba} label="DBA or Tradename (If Applicable)" name="dba" onBlur={handleBlur} onChange={handleChange} value={values.dba} data-testid="ppp-form:input:dba-tradename" /> <Select hasSpaceForErrors isInvalid={touched.businessType && errors.businessType} label="Small Business and Organization Type" name="businessType" onBlur={handleBlur} onChange={handleChange} value={values.businessType} options={[ 'Sole Proprietor', 'Partnership', 'C-Corp', 'S-Corp', 'LLC', 'Independent Contractor', 'Eligible Self Employed', '501(c)(3) Non-Profit', '501(c)(19) Veterans Organization', 'Tribal business (sec. 31(b)(2)(C) of Small Business Act)', 'Other', ]} errors={touched.businessType && [errors.businessType]} data-testid="ppp-form:select:dba-businessType" required /> </StyledStandardCard> <StyledStandardCard> <CardHeader bold> Payroll Information{' '} <StyledIcon data-testid="ppp-form:info-drawer:payroll" name="actions/circle-faq" onClick={() => dispatchOpenInfoDrawer({ children: ( <InfodrawerCopyWrapper data-testid="ppp-form:info-drawer:payroll-costs"> <Copy> <Text $bold>Payroll costs include: </Text> employee salary, wages and commissions; payment of cash tips; payment of vacation; parental, family, medical or sick leave; allowance for dismissal or separation; payment required for group health benefits (including insurance premiums); payment of retirement benefits; or payment of state or local tax assessed on employee compensation; and sole proprietor income or independent contractor compensation not in excess of $100,000. Do not include contractors for whom you provide a 1099; they can apply on their own. </Copy> <Copy> <Text $bold>Payroll costs exclude: </Text> compensation of an individual person in excess of $100,000 (as prorated for the period);; compensation to an employee whose principal residence is outside of the U.S.; Federal employment taxes imposed or withheld between February 15, 2020 and June 30, 2020, including the employee’s and employer’s share of FICA (Federal Insurance Contributions Act) and Railroad Retirement Act taxes, and income taxes required to be withheld from employees; qualified sick or family leave wages for which a credit is allowed under Sections 7001 and 7003 of the Families First Coronavirus Response Act; </Copy> </InfodrawerCopyWrapper> ), title: 'Payroll information', }) } /> </CardHeader> {shouldShowCalculator && ( <CalculatorSpacer> <nav-sba-loan-calculator ref={calcEl} footer="none" style={{ display: 'block' }}> <div slot="cta" /> </nav-sba-loan-calculator> </CalculatorSpacer> )} <Input hasSpaceForErrors type="text" name="avgMonthlyPayroll" onChange={(e) => { const maskedEvent = { ...e } maskedEvent.target.value = toUsdMoney(e.target.value) handleChange(maskedEvent) }} onBlur={handleBlur} value={toUsdMoney(values.avgMonthlyPayroll)} label="Average Monthly Payroll" errors={touched.avgMonthlyPayroll && errors.avgMonthlyPayroll} isInvalid={touched.avgMonthlyPayroll && errors.avgMonthlyPayroll} data-testid="ppp-form:input:avg-monthly-payroll" required /> <ModifiedCardInsight data-testid="ppp-form:payroll-insight"> <IconWrapper> <Icon name="actions/circle-info" onClick={() => dispatchOpenInfoDrawer({ children: ( <InfodrawerCopyWrapper data-testid="ppp-form:info-drawer:eidl"> <Copy> Add the outstanding amount of an Economic Injury Disaster Loan (EIDL) made between January 31, 2020 and April 3, 2020, less the amount of any "advance" under an EIDL COVID-19 loan (because it does not have to be repaid). </Copy> </InfodrawerCopyWrapper> ), title: 'EIDL Net of Advance', }) } /> </IconWrapper> <Copy> <Text $bold>Loan Request Amount:</Text>{' '} <StyledText> Average Monthly Payroll is multiplied 2.5x + If you received an Economic Injury Disaster Loan (EIDL) between January 31, 2020 and April 3, 2020 you must enter the total amount received minus the advance you received from the total. </StyledText> </Copy> </ModifiedCardInsight> <Input hasSpaceForErrors required type="text" name="loanRequestAmount" label="Loan Request Amount" onChange={(e) => { const maskedEvent = { ...e } maskedEvent.target.value = toUsdMoney(e.target.value) handleChange(maskedEvent) }} onBlur={handleBlur} value={toUsdMoney(values.loanRequestAmount)} errors={touched.loanRequestAmount && errors.loanRequestAmount} isInvalid={touched.loanRequestAmount && errors.loanRequestAmount} data-testid="ppp-form:input:loan-amount" /> <Input hasSpaceForErrors type="number" name="numberOfEmployees" onChange={(e) => { const maskedEvent = { ...e } if (maskedEvent.target.value >= 0) { maskedEvent.target.value = toNumber(e.target.value) handleChange(maskedEvent) } }} onBlur={handleBlur} value={toNumber(values.numberOfEmployees)} label="Number of Employees" errors={touched.numberOfEmployees && errors.numberOfEmployees} isInvalid={touched.numberOfEmployees && errors.numberOfEmployees} data-testid="ppp-form:input:number-of-jobs" required /> <InlineHeader bold> Purpose of the loan{' '} <StyledIcon data-testid="ppp-form:info-drawer:purpose" name="actions/circle-faq" onClick={() => dispatchOpenInfoDrawer({ children: ( <InfodrawerCopyWrapper data-testid="ppp-form:info-drawer:what-are-loans-for"> <Copy> You should use the proceeds from these loans on your: Payroll costs, including benefits; Interest on mortgage obligations incurred before February 15, 2020; Rent, under lease agreements in force before February 15, 2020; Utilities, for which service began before February 15, 2020 and refinancing an SBA Economic Injury Disaster Loan made between Jan. 1, 2020 and April 3, 2020. To be eligible for loan forgiveness, no more than 25% of loan proceeds may be used for non-payroll purposes, in addition to other requirements. </Copy> </InfodrawerCopyWrapper> ), title: 'Purpose of the loan', }) } /> </InlineHeader> <Checkbox hasSpaceForErrors name="purposePayroll" label="Payroll" checked={values.purposePayroll} onChange={handleChange} onBlur={handleBlur} errors={touched.purposePayroll && errors.purposePayroll} isInvalid={touched.purposePayroll && errors.purposePayroll} data-testid="ppp-form:checkbox:purpose-payroll" /> <Checkbox hasSpaceForErrors name="purposeRent" label="Lease / Mortgage Interest" checked={values.purposeRent} onChange={handleChange} onBlur={handleBlur} errors={touched.purposeRent && errors.purposeRent} isInvalid={touched.purposeRent && errors.purposeRent} data-testid="ppp-form:checkbox:purpose-rent" /> <Checkbox hasSpaceForErrors name="purposeUtilities" label="Utilities" checked={values.purposeUtilities} onChange={handleChange} onBlur={handleBlur} errors={touched.purposeUtilities && errors.purposeUtilities} isInvalid={touched.purposeUtilities && errors.purposeUtilities} data-testid="ppp-form:checkbox:purpose-utilities" /> <Checkbox hasSpaceForErrors name="purposeOther" label="Other (Explain below)" checked={values.purposeOther} onChange={handleChange} onBlur={handleBlur} errors={touched.purposeOther && errors.purposeOther} isInvalid={touched.purposeOther && errors.purposeOther} data-testid="ppp-form:checkbox:purpose-other" /> <Input hasSpaceForErrors type="text" name="purposeOtherDesc" onChange={handleChange} onBlur={handleBlur} value={values.purposeOtherDesc} label="Explanation" errors={touched.purposeOtherDesc && errors.purposeOtherDesc} isInvalid={touched.purposeOtherDesc && errors.purposeOtherDesc} data-testid="ppp-form:checkbox:purpose-other-desc" required /> </StyledStandardCard> <StyledStandardCard> <CardHeader bold>Applicant Ownership</CardHeader> <EmphasizedCopy>List all owners of 20% or more of the equity of the Applicant.</EmphasizedCopy> <Input hasSpaceForErrors type="text" name="ownerName1" onChange={handleChange} onBlur={handleBlur} value={values.ownerName1} label="Owner Name" errors={touched.ownerName1 && errors.ownerName1} isInvalid={touched.ownerName1 && errors.ownerName1} data-testid="ppp-form:checkbox:purpose-owner-name-1" required /> <Input hasSpaceForErrors type="text" name="ownerTitle1" onChange={handleChange} onBlur={handleBlur} value={values.ownerTitle1} label="Title" errors={touched.ownerTitle1 && errors.ownerTitle1} isInvalid={touched.ownerTitle1 && errors.ownerTitle1} data-testid="ppp-form:checkbox:owner-title-1" required /> <Input hasSpaceForErrors type="text" name="ownerPercent1" onChange={(e) => { if (e.target.value.match(FLOATING_NUMBER_REGEX)) handleChange(e) }} onBlur={(e) => { const maskedEvent = { ...e } const parsed = parseFloat(maskedEvent.target.value) if (parsed) { maskedEvent.target.value = parsed handleChange(maskedEvent) } handleBlur(maskedEvent) }} value={values.ownerPercent1} label="Ownership Percentage" errors={touched.ownerPercent1 && errors.ownerPercent1} isInvalid={touched.ownerPercent1 && errors.ownerPercent1} data-testid="ppp-form:checkbox:owner-percent-1" required /> <Input hasSpaceForErrors type="text" name="ownerAddress1" onChange={handleChange} onBlur={handleBlur} value={values.ownerAddress1} label="Personal Address" errors={touched.ownerAddress1 && errors.ownerAddress1} isInvalid={touched.ownerAddress1 && errors.ownerAddress1} data-testid="ppp-form:checkbox:owner-address-1" required /> <EmphasizedCopy> If questions (1) or (2) below are answered “Yes,” the loan will not be approved. </EmphasizedCopy> <QuestionWrapper> <QuestionCopy> 1. Is the Applicant or any owner of the Applicant presently suspended, debarred, proposed for debarment, declared ineligible, voluntarily excluded from participation in this transaction by any Federal department or agency, or presently involved in any bankruptcy? </QuestionCopy> <RadioGroup> <Radio checked={values.question1 === 'yes'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="yes" name="question1" label="Yes" isInvalid={submitCount > 0 && errors.question1} data-testid="ppp-form:question1-yes" /> <Radio checked={values.question1 === 'no'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="no" name="question1" label="No" isInvalid={submitCount > 0 && errors.question1} data-testid="ppp-form:question1-no" /> </RadioGroup> </QuestionWrapper> <QuestionWrapper> <QuestionCopy> 2. Has the Applicant, any owner of the Applicant, or any business owned or controlled by any of them, ever obtained a direct or guaranteed loan from SBA or any other Federal agency that is currently delinquent or has defaulted in the last 7 years and caused a loss to the government? </QuestionCopy> <RadioGroup> <Radio checked={values.question2 === 'yes'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="yes" name="question2" label="Yes" isInvalid={submitCount > 0 && errors.question2} data-testid="ppp-form:question2-yes" /> <Radio checked={values.question2 === 'no'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="no" name="question2" label="No" isInvalid={submitCount > 0 && errors.question2} data-testid="ppp-form:question2-no" /> </RadioGroup> </QuestionWrapper> <QuestionWrapper> <QuestionCopy> 3. Is the Applicant or any owner of the Applicant an owner of any other business, or have common management with, any other business? If yes, additional details will need to be added in an addendum upon application submission. </QuestionCopy> <RadioGroup> <Radio checked={values.question3 === 'yes'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="yes" name="question3" label="Yes" isInvalid={submitCount > 0 && errors.question3} data-testid="ppp-form:question3-yes" /> <Radio checked={values.question3 === 'no'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="no" name="question3" label="No" isInvalid={submitCount > 0 && errors.question3} data-testid="ppp-form:question3-no" /> </RadioGroup> </QuestionWrapper> <QuestionWrapper> <QuestionCopy> 4. Has the Applicant received an SBA Economic Injury Disaster Loan between January 31, 2020 and April 3, 2020? If yes, additional details will need to be added in an addendum upon application submission. </QuestionCopy> <RadioGroup> <Radio checked={values.question4 === 'yes'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="yes" name="question4" label="Yes" isInvalid={submitCount > 0 && errors.question4} data-testid="ppp-form:question4-yes" /> <Radio checked={values.question4 === 'no'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="no" name="question4" label="No" isInvalid={submitCount > 0 && errors.question4} data-testid="ppp-form:question4-no" /> </RadioGroup> </QuestionWrapper> <FieldArray name="additionalOwners" render={(arrayHelpers) => ( <div> {values?.additionalOwners?.length > 0 ? ( values.additionalOwners.map((owner, index) => { const getFieldName = (name, i) => `additionalOwners.${i}.${name}` const getFieldErrors = (name, i) => touched.additionalOwners?.[i]?.[name] && errors.additionalOwners?.[i]?.[name] return ( <AdditionalOwnersWrapper key={index}> <AdditionalOwnerField> <Field name={getFieldName('name', index)} as={Input} label="Additional Owner Name" type="text" data-testid={`ppp-form:additional-owner-${index}`} errors={getFieldErrors('name', index)} isInvalid={getFieldErrors('name', index)} hasSpaceForErrors required /> </AdditionalOwnerField> <AdditionalOwnerField> <Field name={getFieldName('address', index)} as={Input} label="Personal Address" type="text" data-testid={`ppp-form:additional-owner-address-${index}`} errors={getFieldErrors('address', index)} isInvalid={getFieldErrors('address', index)} hasSpaceForErrors required /> </AdditionalOwnerField> <AdditionalOwnerField> <Field name={getFieldName('title', index)} as={Input} label="Title" type="text" data-testid={`ppp-form:additional-owner-title-${index}`} errors={getFieldErrors('title', index)} isInvalid={getFieldErrors('title', index)} hasSpaceForErrors required /> </AdditionalOwnerField> <AdditionalOwnerField> <Field name={getFieldName('percent', index)} as={Input} label="Ownership Percentage" type="text" onChange={(e) => { if (e.target.value.match(FLOATING_NUMBER_REGEX)) handleChange(e) }} onBlur={(e) => { const maskedEvent = { ...e } const parsed = parseFloat(maskedEvent.target.value) if (parsed) { maskedEvent.target.value = parsed handleChange(maskedEvent) } handleBlur(maskedEvent) }} data-testid={`ppp-form:additional-owner-percent-${index}`} errors={getFieldErrors('percent', index)} isInvalid={getFieldErrors('percent', index)} hasSpaceForErrors required /> </AdditionalOwnerField> <div className="action-buttons"> <StyledIcon name="actions/circle-plus" color="navPrimary" onClick={() => { if (index < 3) arrayHelpers.insert(index + 1, EMPTY_OWNER) }} data-testid="pp-form:additional-owner-plus" /> <StyledIcon name="actions/circle-minus" color="warning" onClick={() => arrayHelpers.remove(index)} data-testid="pp-form:additional-owner-minus" /> </div> </AdditionalOwnersWrapper> ) }) ) : ( <FlexWrapper onClick={() => arrayHelpers.push(EMPTY_OWNER)} data-testid="pp-form:additional-owner-add" > <StyledIcon name="actions/circle-plus" color="navPrimary" /> <AdditionalOwner type="button">Add additional owner</AdditionalOwner> </FlexWrapper> )} </div> )} /> </StyledStandardCard> <StyledStandardCard> <CardHeader bold>Questions</CardHeader> <EmphasizedCopy> If questions (5) or (6) below are answered “Yes,” the loan will not be approved. </EmphasizedCopy> <QuestionWrapper> <QuestionCopy> 5. Is the Applicant (if an individual) or any individual owning 20% or more of the equity of the Applicant subject to an indictment, criminal information, arraignment, or other means by which formal criminal charges are brought in any jurisdiction, or presently incarcerated, or on probation or parole? </QuestionCopy> <RadioGroup> <Radio checked={values.question5 === 'yes'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="yes" name="question5" label="Yes" isInvalid={submitCount > 0 && errors.question5} data-testid="pp-form:question5-yes" /> <Radio checked={values.question5 === 'no'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="no" name="question5" label="No" isInvalid={submitCount > 0 && errors.question5} data-testid="pp-form:question5-no" /> </RadioGroup> </QuestionWrapper> <QuestionWrapper> <QuestionCopy> 6. Within the last 5 years, for any felony, has the Applicant (if an individual) or any owner of the Applicant 1) been convicted; 2) pleaded guilty; 3) pleaded nolo contendere; 4) been placed on pretrial diversion; or 5) been placed on any form of parole or probation (including probation before judgment)? </QuestionCopy> <RadioGroup> <Radio checked={values.question6 === 'yes'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="yes" name="question6" label="Yes" isInvalid={submitCount > 0 && errors.question6} data-testid="pp-form:question6-yes" /> <Radio checked={values.question6 === 'no'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="no" name="question6" label="No" isInvalid={submitCount > 0 && errors.question6} data-testid="pp-form:question6-no" /> </RadioGroup> </QuestionWrapper> <QuestionWrapper> <QuestionCopy> 7. Is the United States the principal place of residence for all employees of the Applicant included in the Applicant’s payroll calculation above? </QuestionCopy> <RadioGroup> <Radio checked={values.question7 === 'yes'} onChange={handleChange} onBlur={handleBlur} onFocus={handleFocus} value="yes" name="question7" label="Yes" isInvalid={submitCount > 0 && er