@navinc/base-react-components
Version:
Nav's Pattern Library
639 lines (615 loc) • 25.5 kB
JavaScript
import React from 'react'
import propTypes from 'prop-types'
import { Formik } from 'formik'
import { toPattern } from 'vanilla-masker'
import { isEmpty, defaultPasswordValidator as passwordValidator } from '@navinc/utils'
import styled, { useTheme } from 'styled-components'
import Input from './input.js'
import Select from './select.js'
import Header from './header.js'
import Copy from './copy.js'
import Text from './text.js'
import USStatesAndTerritories from './data/us-states-and-territories.js'
import {
// getCachedValues,
// setCachedValues,
CardHeader,
CreateAccountHeader,
CreateAccountWrapper,
DisclaimerText,
ErrorsMessage,
InfodrawerCopyWrapper,
LegalCheckbox,
StyledCDNAsset,
StyledForm,
StyledIcon,
StyledStandardCard,
SubmitButton,
TwoColumn,
} from './sba-form.js'
import isRebrand from './is-rebrand.js'
export const StyledHeader = styled(Header)`
margin-bottom: 8px;
`
export const StyledSubHeader = styled(Copy)`
margin-bottom: 16px;
`
const GridRow = styled.div`
display: grid;
grid-template-columns: 1fr;
grid-gap: 16px;
`
const HeaderSection = styled.div`
text-align: center;
padding-bottom: 8px;
`
const CopySection = styled.div`
display: grid;
grid-gap: 16px;
`
const BulletSection = styled.div`
display: grid;
grid-gap: 8px;
grid-template-columns: 32px 1fr;
padding-top: 8px;
`
const FlexDiv = styled.div`
display: flex;
> * {
padding-left: 8px;
padding-bottom: 8px;
}
`
const StyledDiv = styled.div`
padding-bottom: 8px;
`
export const StyledTwoColumn = styled.div`
display: flex;
flex-wrap: wrap;
justify-content: space-between;
${StyledDiv} {
width: 100%;
padding-bottom: 8px;
}
@media (${({ theme }) => theme.forLargerThanPhone}) {
${StyledDiv} {
width: 48%;
}
}
`
const StyledInput = styled(Input)`
display: hidden;
`
const BlueBorderDiv = styled.div`
border: 1px solid ${({ theme }) => (isRebrand(theme) ? 'none' : theme.navPrimary)};
border-radius: 20px;
padding: 20px;
width: 85%;
margin: 10px auto;
`
const HeaderSectionTC = styled.div`
text-align: center;
`
const StyledCopy = styled(Copy)`
padding-bottom: 8px;
`
const entriesPolyFill = (obj) => Object.keys(obj).map((key) => [key, obj[key]])
const usStates = entriesPolyFill(USStatesAndTerritories).map(([label, value]) => ({ label, value }))
export const MiniSBAPaycheckProtectionProgramForm = ({
initialValues = {},
isSubmittingForm = false,
onFormSubmit = () => {},
dispatchOpenInfoDrawer = () => {},
shouldShowPasswordField = false,
headerHelperText = '',
primaryContactNameValue = '',
emailAddressValue = '',
contactPhoneValue = '',
businessLegalNameValue = '',
addressValue = '',
cityValue = '',
stateValue = '',
zipCodeValue = '',
dbaValue = '',
businessTypeValue = '',
termsOfServiceBool = false,
secondDraw = '',
shouldShowHasReceivedSecondPPPDraw = false,
termsHref = '',
shouldShowDocumentation = false,
privatePolicy = '',
isClover = false,
referrerID = '',
}) => {
const theme = useTheme()
const defaultDirectory = isRebrand(theme) ? 'illustrations' : 'design-assets/illustrations'
const hiddenValue = isClover ? 'Clover' : referrerID
// const { callback: debouncedSetCachedValues = () => {} } = useDebouncedCallback(setCachedValues, 750)
const handleSubmit = ({ emailAddress, password, ...rest } = {}) => {
const cleanedValues = {
version: 4, // Increment the version each time the shape of data changes
emailAddress,
...rest,
}
const loginInfo = shouldShowPasswordField && { emailAddress, password }
onFormSubmit(cleanedValues, loginInfo)
}
return (
<Formik
enableReinitialize
initialValues={{
primaryContact: primaryContactNameValue || '',
emailAddress: emailAddressValue || '',
primaryContactPhone: contactPhoneValue || '',
businessLegalName: businessLegalNameValue || '',
secondDraw: secondDraw || '',
address: addressValue || '',
city: cityValue || '',
state: stateValue || '',
zipCode: zipCodeValue || '',
dba: dbaValue || '',
businessType: businessTypeValue || '',
password: '',
termsOfService: termsOfServiceBool,
partnerField: hiddenValue,
...initialValues,
// ...getCachedValues(), // Pre-fill the form with any cached values we can find
}}
onSubmit={handleSubmit}
validate={({ password, ...restOfValues }) => {
// debouncedSetCachedValues(restOfValues) // Update the cache as things change
const {
primaryContact,
emailAddress,
primaryContactPhone,
businessLegalName,
address,
city,
state,
zipCode,
businessType,
termsOfService,
secondDraw,
} = restOfValues
const PHONE_REGEX = /^\d{3}-\d{3}-\d{4}$/
const REQUIRED_MESSAGE = 'Required'
const PHONE_MESSAGE = 'Phone number is invalid'
return {
...(!primaryContact && {
primaryContact: [REQUIRED_MESSAGE],
}),
...(primaryContact.length > 121 && {
primaryContact: ['Primary Contact must be 121 characters or less'],
}),
...(!emailAddress.match(
// eslint-disable-next-line no-control-regex
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/
) && {
emailAddress: ['Enter a valid email'],
}),
...(!primaryContactPhone.match(PHONE_REGEX) && {
primaryContactPhone: [PHONE_MESSAGE],
}),
...(primaryContactPhone &&
primaryContactPhone === 'xxx-xxx-xxxx' && {
primaryContactPhone: [REQUIRED_MESSAGE],
}),
...(!businessLegalName && { businessLegalName: [REQUIRED_MESSAGE] }),
...(businessLegalName.length > 255 && {
businessLegalName: ['Business Name must be 255 characters or less'],
}),
...(!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] }),
...(shouldShowHasReceivedSecondPPPDraw && !secondDraw && { secondDraw: [REQUIRED_MESSAGE] }),
...(!termsOfService && { termsOfService: [REQUIRED_MESSAGE] }),
...(shouldShowPasswordField &&
passwordValidator(password).length && {
password: passwordValidator(password),
}),
}
}}
>
{({ values, isValid, errors, touched, handleChange, handleBlur, handleSubmit, submitCount }) => (
<StyledForm data-testid="paycheck-protection-program:form" onSubmit={handleSubmit}>
<StyledHeader size="xl">Paycheck Protection Program (PPP)</StyledHeader>
<StyledSubHeader>
{headerHelperText ||
'Additional PPP funds will be made available in January 2021. Get started on your payment protection program (PPP) application today.'}
</StyledSubHeader>
<StyledStandardCard>
<CardHeader bold>
Primary Contact Information{' '}
{!isClover && (
<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>
<StyledDiv>
<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 ? errors.primaryContact : undefined}
data-testid="ppp-form:input:primary-contact"
/>
</StyledDiv>
<StyledDiv>
<Input
hasSpaceForErrors
label="Best Email Address"
type="email"
name="emailAddress"
value={values.emailAddress}
onBlur={handleBlur}
onChange={handleChange}
isInvalid={touched.emailAddress && !!errors?.emailAddress}
errors={touched.emailAddress && errors.emailAddress ? errors.emailAddress : undefined}
data-testid="ppp-form:input:primary-email-address"
/>
<FlexDiv>
<Copy size="xs" light bold>
Use the same email in your PPP loan application.{' '}
</Copy>
</FlexDiv>
</StyledDiv>
<StyledDiv>
<Input
hasSpaceForErrors
label="Best Contact Phone Number"
placeholder="xxx-xxx-xxxx"
name="primaryContactPhone"
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 ? errors.primaryContactPhone : undefined
}
data-testid="ppp-form:input:primary-contact-phone"
/>
<FlexDiv>
<Copy size="xs" light bold>
Please enter your contact phone number above.{' '}
</Copy>
</FlexDiv>
</StyledDiv>
{shouldShowHasReceivedSecondPPPDraw && (
<GridRow>
<Copy bold>Have you previously received PPP funds?</Copy>
<Select
hasSpaceForErrors
isInvalid={touched.secondDraw && !!errors?.secondDraw}
label="Please make a selection"
name="secondDraw"
onBlur={handleBlur}
onChange={handleChange}
value={values.secondDraw}
options={['Yes', 'No']}
errors={touched.secondDraw && !!errors?.secondDraw ? errors.secondDraw : undefined}
data-testid="ppp-form:select:secondDraw"
/>
</GridRow>
)}
</StyledStandardCard>
<StyledStandardCard>
<CardHeader bold>
Business Information{' '}
{!isClover && (
<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 ? errors.businessLegalName : undefined}
data-testid="ppp-form:input:business-legal-name"
/>
<Input
errors={touched.address && errors.address ? errors.address : undefined}
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"
/>
<Input
errors={touched.city && errors?.city ? errors.city : undefined}
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"
/>
<TwoColumn>
<Select
errors={touched.state && errors.state ? errors.state : undefined}
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"
/>
<Input
errors={touched.zipCode && errors.zipCode ? errors.zipCode : undefined}
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"
/>
</TwoColumn>
<Select
hasSpaceForErrors
isInvalid={touched.businessType && !!errors?.businessType}
label="Entity 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 ? errors.businessType : undefined}
data-testid="ppp-form:select:dba-businessType"
/>
<StyledInput type="hidden" name="partnerField" value={values.partnerField} />
</StyledStandardCard>
{shouldShowDocumentation && (
<StyledStandardCard>
<HeaderSection>
<Header size="lg">Documentation to have ready</Header>
<Copy>
Use the following checklist to gather the documentation that you need to complete your PPP loan
application. Having these ready will speed up the application process.
</Copy>
</HeaderSection>
<BulletSection>
<div />
<CopySection>
<Header bold>
SBA Loan Number of First PPP loan (Applies only to second-draw PPP loan applicants)
</Header>
<StyledCopy>
Be sure to accurately indicate whether you are applying for a first-draw or second-draw PPP loan. If
you are applying for a second-draw PPP loan, you must provide the SBA loan number for the first PPP
loan you obtained in 2020. SBA loan numbers (PLP) have eight numbers followed by a dash then two
more numbers (i.e., XXXXXXXX-XX).
</StyledCopy>
</CopySection>
</BulletSection>
<BulletSection>
<div />
<CopySection>
<Header bold>CARES Act Report - Only for those with payroll (Not self-employed)</Header>
<StyledCopy>
If you're using a payroll provider they should be able to provide you a CARES Act report. Having
this ready allows your loan processing team to review your loan quicker.{' '}
</StyledCopy>
</CopySection>
</BulletSection>
<BulletSection>
<div />
<CopySection>
<Header size="sm">Tax Documents for 2019 (or 2020 if using 2020 payroll)</Header>
<StyledCopy>
If you don’t have a CARES Act report, you may upload tax documents instead. The required tax
document will depend on your business’s entity type.
</StyledCopy>
</CopySection>
</BulletSection>
<BulletSection>
<div />
<CopySection>
<Header size="sm">
Documentation of Reduction in Receipts (Applies only to second-draw PPP loans over $150,000)
</Header>
<StyledCopy>
For a second-draw PPP loan, you must provide documentation showing at least a 25% reduction in gross
receipts annually or in at least one quarter in 2020 when compared to the same quarter in 2019.
(Alternative calculations may apply for those not in business all of 2019.) Acceptable documentation
may include quarterly financial statements; quarterly or monthly bank statements showing deposits
from the relevant quarters; or annual IRS income tax filings (if using an annual reference period).
</StyledCopy>
</CopySection>
</BulletSection>
<BulletSection>
<div />
<CopySection>
<Header size="sm">Bank Account Information</Header>
<StyledCopy>
You will need the account number and routing number for your business checking account. You can find
this information on your bank statement, by logging into your bank’s website, or from one of your
business checks.
</StyledCopy>
</CopySection>
</BulletSection>
<BulletSection>
<div />
<CopySection>
<Header size="sm">Driver’s License or Passport Number</Header>
<StyledCopy>
Have your driver’s license or passport handy so you can enter the identification number of one of
them on your loan application.
</StyledCopy>
</CopySection>
</BulletSection>
</StyledStandardCard>
)}
<StyledStandardCard>
{shouldShowPasswordField && (
<CreateAccountWrapper>
<StyledCDNAsset directory={defaultDirectory} height="126px" filename="CARES-app-thank+you.png" />
<CreateAccountHeader>
Save all your information to a free Nav account and access more financing options available for your
business.
</CreateAccountHeader>
<Input
hasSpaceForErrors
type="password"
name="password"
onChange={handleChange}
onBlur={handleBlur}
value={values.password}
label="Password"
errors={touched.password && errors.password ? errors.password : undefined}
isInvalid={touched.password && !!errors?.password}
data-testid="ppp-form:input:password"
/>
</CreateAccountWrapper>
)}
<BlueBorderDiv>
<HeaderSectionTC>
<Header>Check the box before submitting</Header>
</HeaderSectionTC>
<LegalCheckbox
hasSpaceForErrors
name="termsOfService"
label={
<Copy>
{privatePolicy ||
`Information is gathered in accordance with Nav’s Privacy Policy. By checking this box, I confirm that
I have read and agree to Nav’s `}
<a
href={termsHref || `https://www.nav.com/terms/`}
target="_blank"
rel="noopener noreferrer"
data-testid="pp-form:terms-service-link"
>
Terms of Service
</a>
, including the{' '}
<a
href="https://www.nav.com/privacy/"
target="_blank"
rel="noopener noreferrer"
data-testid="pp-form:privacy-link"
>
Privacy Policy
</a>
.
</Copy>
}
checked={values.termsOfService}
onChange={handleChange}
onBlur={handleBlur}
errors={touched.termsOfService && errors.termsOfService ? errors.termsOfService : undefined}
isInvalid={touched.termsOfService && !!errors?.termsOfService}
data-testid="pp-form:terms-of-service"
/>
{!isEmpty(errors.totalPercent) && <ErrorsMessage>{errors.totalPercent[0]}</ErrorsMessage>}
{/* Show the error message only if there are errors and a submit has been attempted */}
{!!Object.keys(errors).length && !isValid && submitCount > 0 && (
<ErrorsMessage>Please complete all required fields before submitting your application.</ErrorsMessage>
)}
<SubmitButton
size="extraLarge"
type="submit"
// Disable only if submit has been attempted before
disabled={(isSubmittingForm || !!Object.keys(errors).length) && submitCount}
data-testid="pp-form:submit-cta"
isLoading={isSubmittingForm}
>
Submit
</SubmitButton>
</BlueBorderDiv>
<DisclaimerText light>
Please keep in mind this information is changing rapidly and is based on our current understanding of the
programs. Please do not rely solely on information provided here for your financial decisions.
</DisclaimerText>
</StyledStandardCard>
</StyledForm>
)}
</Formik>
)
}
MiniSBAPaycheckProtectionProgramForm.propTypes = {
initialValues: propTypes.object,
isSubmittingForm: propTypes.bool,
onFormSubmit: propTypes.func.isRequired,
dispatchOpenInfoDrawer: propTypes.func.isRequired,
shouldShowLoginPrompt: propTypes.bool,
loginPromptUrl: propTypes.string,
onLoginPromptDismiss: propTypes.func,
shouldShowPasswordField: propTypes.bool,
}