client/components/mma/paymentUpdate/dd/DirectDebitInputForm.tsx (257 lines of code) (raw):

import { css } from '@emotion/react'; import { from, space, textSans17, until } from '@guardian/source/foundations'; import { Button, Checkbox, SvgArrowRightStraight, } from '@guardian/source/react-components'; import { ErrorSummary } from '@guardian/source-development-kitchen/react-components'; import { useState } from 'react'; import type { DirectDebitGatewayOwner } from '@/shared/directDebit'; import { processResponse } from '../../../../utilities/utils'; import { cleanSortCode } from '../../shared/DirectDebitDisplay'; import type { FieldChangeEvent } from '../FieldWrapper'; import { FieldWrapper } from '../FieldWrapper'; import type { NewPaymentMethodDetail } from '../NewPaymentMethodDetail'; import { DirectDebitLegal } from './DirectDebitLegal'; import { NewDirectDebitPaymentMethodDetail } from './NewDirectDebitPaymentMethodDetail'; const inputBoxBaseStyle = css` width: 100%; height: 100%; ${textSans17}; border: none; outline: none; ::placeholder: { color: #c4c4c4; } :-ms-input-placeholder: { color: #c4c4c4; } `; const bulletsStyling = css` ::placeholder { font-size: 14px; } :-ms-input-placeholder { font-size: 14px; } `; interface DirectDebitValidationResponse { data: { accountValid: boolean; goCardlessStatusCode: null | number; }; } interface DirectDebitUpdateFormProps { newPaymentMethodDetailUpdater: (ddDetails: NewPaymentMethodDetail) => void; testUser: boolean; executePaymentUpdate: ( newPaymentMethodDetail: NewPaymentMethodDetail, ) => Promise<unknown>; gatewayOwner?: DirectDebitGatewayOwner; } export const DirectDebitInputForm = (props: DirectDebitUpdateFormProps) => { const [isValidating, setIsValidating] = useState<boolean>(false); const [soleAccountHolderConfirmed, setSoleAccountHolderConfirmed] = useState<boolean>(false); const [accountName, setAccountName] = useState<string>(''); const [accountNumber, setAccountNumber] = useState<string>(''); const [sortCode, setSortCode] = useState<string>(''); const [error, setError] = useState<string | undefined>(); async function validateDirectDebitDetails( newPaymentMethod: NewPaymentMethodDetail, ) { try { const validateDirectDebitDetailsFetch = await fetch( `/api/validate/payment/dd?mode=${ props.testUser ? 'test' : 'live' }`, { credentials: 'include', method: 'POST', body: JSON.stringify({ accountNumber, sortCode: cleanSortCode(sortCode), }), headers: { 'Content-Type': 'application/json' }, }, ); const response = await processResponse<DirectDebitValidationResponse>( validateDirectDebitDetailsFetch, ); if (response && response.data.accountValid) { setIsValidating(false); props.executePaymentUpdate(newPaymentMethod); } else if (response && response.data.goCardlessStatusCode === 429) { setIsValidating(false); setError( 'We cannot currently validate your bank details. Please try again later.', ); } else { setIsValidating(false); setError( 'Your bank details are invalid. Please check them and try again.', ); } } catch { setIsValidating(false); setError( 'Could not validate your bank details, please check them and try again.', ); } } const startDirectDebitUpdate = async () => { const newPaymentMethod = new NewDirectDebitPaymentMethodDetail({ accountName, accountNumber, sortCode, gatewayOwner: props.gatewayOwner, }); props.newPaymentMethodDetailUpdater(newPaymentMethod); setError(undefined); if (accountName.length < 3) { setError('Please enter a valid account name'); // TODO add field highlighting return; } else if (soleAccountHolderConfirmed) { setIsValidating(true); } else { setError('You need to confirm that you are the account holder'); // TODO highlight checkbox return; } validateDirectDebitDetails(newPaymentMethod); }; return ( <div css={css` margin-top: ${space[9]}px; margin-bottom: ${space[9]}px; `} > <FieldWrapper width="100%" label="Account holder name" onChange={(event: FieldChangeEvent) => setAccountName(event.target.value) } > <input data-qm-masking="blocklist" type="text" css={inputBoxBaseStyle} placeholder="First Name Surname" name="Account holder name" pattern="[A-Za-z\s]{3,}" title="The name of the account holder must have at least 3 letters." required /> </FieldWrapper> <div css={css` display: flex; justify-content: flex-start; ${from.tablet} { margin-top: ${space[4]}px; } `} > <FieldWrapper width="220px" label="Sort Code" onChange={(event: FieldChangeEvent) => setSortCode(cleanSortCode(event.target.value)) } > <input data-qm-masking="blocklist" type="text" pattern="[0-9]{2}[\-\s]?[0-9]{2}[\-\s]?[0-9]{2}" title="Sort Code must contain 6 numbers (optionally separated by a - or space)" css={css` ${bulletsStyling}; ${inputBoxBaseStyle} `} placeholder="•• •• ••" name="Sort Code" required /> </FieldWrapper> <FieldWrapper width="100%" label="Account Number" onChange={(event: FieldChangeEvent) => setAccountNumber(event.target.value) } > <input data-qm-masking="blocklist" type="text" pattern="[0-9]{7,}" css={css` ${bulletsStyling}; ${inputBoxBaseStyle} `} placeholder="•••• ••••" name="Account Number" title="Account Number should typically be 8 digits" required /> </FieldWrapper>{' '} </div> <div css={css` margin: 14px 0; ${from.tablet} { margin: 4px 0; } `} > <Checkbox onChange={(e) => setSoleAccountHolderConfirmed(e.target.checked) } checked={soleAccountHolderConfirmed} label="I confirm that I am the account holder and I am solely able to authorise debit from the account" required name="accountHolderConfirmation" value="I confirm that I am the account holder and I am solely able to authorise debit from the account" cssOverrides={css` & + span { top: calc(50% - 8px); } `} /> </div> <DirectDebitLegal /> <div css={css` margin-top: ${space[9]}px; margin-bottom: ${space[9]}px; ${until.desktop} { width: 100%; } `} > <Button disabled={isValidating} priority="primary" onClick={startDirectDebitUpdate} icon={<SvgArrowRightStraight />} iconSide="right" > Update payment method </Button> {error ? ( <div css={css` margin-top: ${space[9]}px; margin-bottom: ${space[9]}px; `} > <ErrorSummary message={error} /> </div> ) : undefined} </div> </div> ); };