@navinc/base-react-components
Version:
Nav's Pattern Library
1,305 lines (1,229 loc) • 55.6 kB
JavaScript
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