diff --git a/src/components/UploadFile.tsx b/src/components/UploadFile.tsx index 5dff344a5877..f434b08d3800 100644 --- a/src/components/UploadFile.tsx +++ b/src/components/UploadFile.tsx @@ -69,7 +69,9 @@ function UploadFile({ const theme = useTheme(); const handleFileUpload = (files: FileObject[]) => { - const totalSize = files.reduce((sum, file) => sum + (file.size ?? 0), 0); + const resultedFiles = [...uploadedFiles, ...files]; + + const totalSize = resultedFiles.reduce((sum, file) => sum + (file.size ?? 0), 0); if (totalFilesSizeLimit) { if (totalSize > totalFilesSizeLimit) { @@ -78,7 +80,7 @@ function UploadFile({ } } - if (fileLimit && files.length > 0 && files.length > fileLimit) { + if (fileLimit && resultedFiles.length > 0 && resultedFiles.length > fileLimit) { setError(translate('attachmentPicker.tooManyFiles', {fileLimit})); return; } @@ -98,6 +100,7 @@ function UploadFile({ onInputChange(newFilesToUpload); onUpload(newFilesToUpload); + setError(''); }; return ( @@ -123,7 +126,7 @@ function UploadFile({ {uploadedFiles.map((file) => ( {file.name} onRemove(file?.uri ?? '')} + onPress={() => onRemove(file?.name ?? '')} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.remove')} > @@ -151,7 +154,7 @@ function UploadFile({ ))} {errorText !== '' && ( diff --git a/src/languages/en.ts b/src/languages/en.ts index d13cf61957ea..4df23b1e4db6 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -83,6 +83,7 @@ import type { InvalidPropertyParams, InvalidValueParams, IssueVirtualCardParams, + LastFourDigitsParams, LastSyncAccountingParams, LastSyncDateParams, LocalTimeParams, @@ -2289,6 +2290,16 @@ const translations = { findCountry: 'Find country', selectCountry: 'Select country', }, + bankInfoStep: { + whatAreYour: 'What are your business bank account details?', + letsDoubleCheck: 'Let’s double check that everything looks fine.', + thisBankAccount: 'This bank account will be used for business payments on your workspace', + accountNumber: 'Account number', + bankStatement: 'Bank statement', + chooseFile: 'Choose file', + uploadYourLatest: 'Upload your latest statement', + pleaseUpload: ({lastFourDigits}: LastFourDigitsParams) => `Please upload the most recent monthly statement for your business bank account ending in ${lastFourDigits}.`, + }, signerInfoStep: { signerInfo: 'Signer info', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index d0ca8bc173bd..62e38a062dd8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -81,6 +81,7 @@ import type { InvalidPropertyParams, InvalidValueParams, IssueVirtualCardParams, + LastFourDigitsParams, LastSyncAccountingParams, LastSyncDateParams, LocalTimeParams, @@ -2313,6 +2314,16 @@ const translations = { findCountry: 'Encontrar país', selectCountry: 'Seleccione su país', }, + bankInfoStep: { + whatAreYour: '¿Cuáles son los detalles de tu cuenta bancaria comercial?', + letsDoubleCheck: 'Verifiquemos que todo esté bien.', + thisBankAccount: 'Esta cuenta bancaria se utilizará para pagos comerciales en tu espacio de trabajo.', + accountNumber: 'Número de cuenta', + bankStatement: 'Extracto bancario', + chooseFile: 'Elegir archivo', + uploadYourLatest: '¿Cuáles son los detalles de tu cuenta bancaria comercial?', + pleaseUpload: ({lastFourDigits}: LastFourDigitsParams) => `Por favor suba el estado de cuenta mensual más reciente de tu cuenta bancaria comercial que termina en ${lastFourDigits}.`, + }, signerInfoStep: { signerInfo: 'Información del firmante', }, diff --git a/src/languages/params.ts b/src/languages/params.ts index 2d60c13c4dd0..7574fe96bd60 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -543,6 +543,10 @@ type FileLimitParams = { fileLimit: number; }; +type LastFourDigitsParams = { + lastFourDigits: string; +}; + type CompanyCardBankName = { bankName: string; }; @@ -641,6 +645,7 @@ export type { HeldRequestParams, InstantSummaryParams, IssueVirtualCardParams, + LastFourDigitsParams, LocalTimeParams, LogSizeParams, LoggedInAsParams, diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 6679a6e4b9ea..bac1dba9ec71 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -343,6 +343,163 @@ function validateBankAccount(bankAccountID: number, validateCode: string, policy API.write(WRITE_COMMANDS.VALIDATE_BANK_ACCOUNT_WITH_TRANSACTIONS, parameters, onyxData); } +function getCorpayBankAccountFields(country: string, currency: string) { + // TODO - Use parameters when API is ready + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const parameters = { + countryISO: country, + currency, + isWithdrawal: true, + isBusinessBankAccount: true, + }; + + // return API.read(READ_COMMANDS.GET_CORPAY_BANK_ACCOUNT_FIELDS, parameters); + return { + bankCountry: 'AU', + bankCurrency: 'AUD', + classification: 'Business', + destinationCountry: 'AU', + formFields: [ + { + errorMessage: 'Swift must be less than 12 characters', + id: 'swiftBicCode', + isRequired: false, + isRequiredInValueSet: true, + label: 'Swift Code', + regEx: '^.{0,12}$', + validationRules: [ + { + errorMessage: 'Swift must be less than 12 characters', + regEx: '^.{0,12}$', + }, + { + errorMessage: 'The following characters are not allowed: <,>, "', + regEx: '^[^<>\\x22]*$', + }, + ], + }, + { + errorMessage: 'Beneficiary Bank Name must be less than 250 characters', + id: 'bankName', + isRequired: true, + isRequiredInValueSet: true, + label: 'Bank Name', + regEx: '^.{0,250}$', + validationRules: [ + { + errorMessage: 'Beneficiary Bank Name must be less than 250 characters', + regEx: '^.{0,250}$', + }, + { + errorMessage: 'The following characters are not allowed: <,>, "', + regEx: '^[^<>\\x22]*$', + }, + ], + }, + { + errorMessage: 'City must be less than 100 characters', + id: 'bankCity', + isRequired: true, + isRequiredInValueSet: true, + label: 'Bank City', + regEx: '^.{0,100}$', + validationRules: [ + { + errorMessage: 'City must be less than 100 characters', + regEx: '^.{0,100}$', + }, + { + errorMessage: 'The following characters are not allowed: <,>, "', + regEx: '^[^<>\\x22]*$', + }, + ], + }, + { + errorMessage: 'Bank Address Line 1 must be less than 1000 characters', + id: 'bankAddressLine1', + isRequired: true, + isRequiredInValueSet: true, + label: 'Bank Address', + regEx: '^.{0,1000}$', + validationRules: [ + { + errorMessage: 'Bank Address Line 1 must be less than 1000 characters', + regEx: '^.{0,1000}$', + }, + { + errorMessage: 'The following characters are not allowed: <,>, "', + regEx: '^[^<>\\x22]*$', + }, + ], + }, + { + detailedRule: [ + { + isRequired: true, + value: [ + { + errorMessage: 'Beneficiary Account Number is invalid. Value should be 1 to 50 characters long.', + regEx: '^.{1,50}$', + ruleDescription: '1 to 50 characters', + }, + ], + }, + ], + errorMessage: 'Beneficiary Account Number is invalid. Value should be 1 to 50 characters long.', + id: 'accountNumber', + isRequired: true, + isRequiredInValueSet: true, + label: 'Account Number (iACH)', + regEx: '^.{1,50}$', + validationRules: [ + { + errorMessage: 'Beneficiary Account Number is invalid. Value should be 1 to 50 characters long.', + regEx: '^.{1,50}$', + ruleDescription: '1 to 50 characters', + }, + { + errorMessage: 'The following characters are not allowed: <,>, "', + regEx: '^[^<>\\x22]*$', + }, + ], + }, + { + detailedRule: [ + { + isRequired: true, + value: [ + { + errorMessage: 'BSB Number is invalid. Value should be exactly 6 digits long.', + regEx: '^[0-9]{6}$', + ruleDescription: 'Exactly 6 digits', + }, + ], + }, + ], + errorMessage: 'BSB Number is invalid. Value should be exactly 6 digits long.', + id: 'routingCode', + isRequired: true, + isRequiredInValueSet: true, + label: 'BSB Number', + regEx: '^[0-9]{6}$', + validationRules: [ + { + errorMessage: 'BSB Number is invalid. Value should be exactly 6 digits long.', + regEx: '^[0-9]{6}$', + ruleDescription: 'Exactly 6 digits', + }, + { + errorMessage: 'The following characters are not allowed: <,>, "', + regEx: '^[^<>\\x22]*$', + }, + ], + }, + ], + paymentMethods: ['E'], + preferredMethod: 'E', + }; +} + function clearReimbursementAccount() { Onyx.set(ONYXKEYS.REIMBURSEMENT_ACCOUNT, null); } @@ -572,6 +729,7 @@ export { updateAddPersonalBankAccountDraft, clearPersonalBankAccountSetupType, validatePlaidSelection, + getCorpayBankAccountFields, }; export type {BusinessAddress, PersonalAddress}; diff --git a/src/pages/ReimbursementAccount/NonUSD/BankInfo/BankInfo.tsx b/src/pages/ReimbursementAccount/NonUSD/BankInfo/BankInfo.tsx index d6a9267b4f94..7c5d853428c5 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BankInfo/BankInfo.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BankInfo/BankInfo.tsx @@ -1,11 +1,17 @@ import type {ComponentType} from 'react'; -import React from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import useLocalize from '@hooks/useLocalize'; import useSubStep from '@hooks/useSubStep'; -import type {SubStepProps} from '@hooks/useSubStep/types'; +import * as BankAccounts from '@userActions/BankAccounts'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; +import BankAccountDetails from './substeps/BankAccountDetails'; import Confirmation from './substeps/Confirmation'; +import UploadStatement from './substeps/UploadStatement'; +import type {BankInfoSubStepProps, CorpayFormField} from './types'; type BankInfoProps = { /** Handles back button press */ @@ -15,16 +21,41 @@ type BankInfoProps = { onSubmit: () => void; }; -const bodyContent: Array> = [Confirmation]; +const {COUNTRY} = INPUT_IDS.ADDITIONAL_DATA; + +const bodyContent: Array> = [BankAccountDetails, UploadStatement, Confirmation]; function BankInfo({onBackButtonPress, onSubmit}: BankInfoProps) { const {translate} = useLocalize(); + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + const [corpayFields, setCorpayFields] = useState([]); + const country = reimbursementAccountDraft?.[COUNTRY] ?? ''; + const policyID = reimbursementAccount?.achData?.policyID ?? '-1'; + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const currency = policy?.outputCurrency ?? ''; + const submit = () => { onSubmit(); }; - const {componentToRender: SubStep, isEditing, screenIndex, nextScreen, prevScreen, moveTo, goToTheLastStep} = useSubStep({bodyContent, startFrom: 0, onFinished: submit}); + const { + componentToRender: SubStep, + isEditing, + screenIndex, + nextScreen, + prevScreen, + moveTo, + goToTheLastStep, + resetScreenIndex, + } = useSubStep({bodyContent, startFrom: 0, onFinished: submit}); + + // Temporary solution to get the fields for the corpay bank account fields + useEffect(() => { + const response = BankAccounts.getCorpayBankAccountFields(country, currency); + setCorpayFields((response?.formFields as CorpayFormField[]) ?? []); + }, [country, currency]); const handleBackButtonPress = () => { if (isEditing) { @@ -34,11 +65,27 @@ function BankInfo({onBackButtonPress, onSubmit}: BankInfoProps) { if (screenIndex === 0) { onBackButtonPress(); - } else { + } else if (currency === CONST.CURRENCY.AUD) { prevScreen(); + } else { + resetScreenIndex(); } }; + const handleNextScreen = useCallback(() => { + if (screenIndex === 2) { + nextScreen(); + return; + } + + if (currency !== CONST.CURRENCY.AUD) { + goToTheLastStep(); + return; + } + + nextScreen(); + }, [currency, goToTheLastStep, nextScreen, screenIndex]); + return ( ); diff --git a/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/BankAccountDetails.tsx b/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/BankAccountDetails.tsx new file mode 100644 index 000000000000..b3482a516c1f --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/BankAccountDetails.tsx @@ -0,0 +1,94 @@ +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxKeys, FormOnyxValues} from '@components/Form/types'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {BankInfoSubStepProps} from '@pages/ReimbursementAccount/NonUSD/BankInfo/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +function BankAccountDetails({onNext, isEditing, corpayFields}: BankInfoSubStepProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const fieldIds = corpayFields.map((field) => field.id); + + const validate = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const errors: FormInputErrors = {}; + + corpayFields.forEach((field) => { + const fieldID = field.id as keyof FormOnyxValues; + + if (field.isRequired && !values[fieldID]) { + errors[fieldID] = translate('common.error.fieldRequired'); + } + + field.validationRules.forEach((rule) => { + if (rule.regEx) { + return; + } + + if (new RegExp(rule.regEx).test(values[fieldID] ? String(values[fieldID]) : '')) { + return; + } + + errors[fieldID] = rule.errorMessage; + }); + }); + + return errors; + }, + [corpayFields, translate], + ); + + const handleSubmit = useReimbursementAccountStepFormSubmit({ + fieldIds: fieldIds as Array>, + onNext, + shouldSaveDraft: isEditing, + }); + + const inputs = useMemo(() => { + return corpayFields.map((field) => { + return ( + + + + ); + }); + }, [corpayFields, styles.flex2, styles.mb6, isEditing]); + + return ( + + + {translate('bankInfoStep.whatAreYour')} + {inputs} + + + ); +} + +BankAccountDetails.displayName = 'BankAccountDetails'; + +export default BankAccountDetails; diff --git a/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/Confirmation.tsx index 9ff2b0e57de9..8abe5e41aaaf 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BankInfo/substeps/Confirmation.tsx @@ -1,16 +1,63 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import Button from '@components/Button'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; -import type {SubStepProps} from '@hooks/useSubStep/types'; import useThemeStyles from '@hooks/useThemeStyles'; +import type {BankInfoSubStepProps} from '@pages/ReimbursementAccount/NonUSD/BankInfo/types'; +import getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReimbursementAccountForm} from '@src/types/form/ReimbursementAccountForm'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -function Confirmation({onNext}: SubStepProps) { +function Confirmation({onNext, onMove, corpayFields}: BankInfoSubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + const inputKeys = useMemo(() => { + const keys: Record = {}; + corpayFields.forEach((field) => { + keys[field.id] = field.id; + }); + return keys; + }, [corpayFields]); + const values = useMemo(() => getSubstepValues(inputKeys, reimbursementAccountDraft, reimbursementAccount), [inputKeys, reimbursementAccount, reimbursementAccountDraft]); + + const items = useMemo( + () => ( + <> + {corpayFields.map((field) => { + return ( + { + onMove(0); + }} + key={field.id} + /> + ); + })} + {!!reimbursementAccountDraft?.[INPUT_IDS.ADDITIONAL_DATA.CORPAY.BANK_STATEMENT] && ( + file.name).join(', ')} + shouldShowRightIcon + onPress={() => onMove(1)} + /> + )} + + ), + [corpayFields, onMove, reimbursementAccountDraft, translate, values], + ); + return ( {({safeAreaPaddingBottomStyle}) => ( @@ -18,6 +65,9 @@ function Confirmation({onNext}: SubStepProps) { style={styles.pt0} contentContainerStyle={[styles.flexGrow1, safeAreaPaddingBottomStyle]} > + {translate('bankInfoStep.letsDoubleCheck')} + {translate('bankInfoStep.thisBankAccount')} + {items}