diff --git a/src/CONST.ts b/src/CONST.ts index faa3d35a5e06..986296ad3db8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -620,6 +620,31 @@ const CONST = { AGREEMENTS: 'AgreementsStep', FINISH: 'FinishStep', }, + BENEFICIAL_OWNER_INFO_STEP: { + SUBSTEP: { + IS_USER_BENEFICIAL_OWNER: 1, + IS_ANYONE_ELSE_BENEFICIAL_OWNER: 2, + BENEFICIAL_OWNER_DETAILS_FORM: 3, + ARE_THERE_MORE_BENEFICIAL_OWNERS: 4, + OWNERSHIP_CHART: 5, + BENEFICIAL_OWNERS_LIST: 6, + }, + BENEFICIAL_OWNER_DATA: { + BENEFICIAL_OWNER_KEYS: 'beneficialOwnerKeys', + PREFIX: 'beneficialOwner', + FIRST_NAME: 'firstName', + LAST_NAME: 'lastName', + OWNERSHIP_PERCENTAGE: 'ownershipPercentage', + DOB: 'dob', + SSN_LAST_4: 'ssnLast4', + STREET: 'street', + CITY: 'city', + STATE: 'state', + ZIP_CODE: 'zipCode', + COUNTRY: 'country', + }, + CURRENT_USER_KEY: 'currentUser', + }, STEP_NAMES: ['1', '2', '3', '4', '5', '6'], STEP_HEADER_HEIGHT: 40, SIGNER_INFO_STEP: { diff --git a/src/languages/en.ts b/src/languages/en.ts index e64fafd008b0..591f7eb0ed42 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1954,6 +1954,7 @@ const translations = { noDefaultDepositAccountOrDebitCardAvailable: 'Please add a default deposit account or debit card.', validationAmounts: 'The validation amounts you entered are incorrect. Please double check your bank statement and try again.', fullName: 'Please enter a valid full name.', + ownershipPercentage: 'Please enter a valid percentage number.', }, }, addPersonalBankAccountPage: { @@ -2230,6 +2231,43 @@ const translations = { byAddingThisBankAccount: "By adding this bank account, you confirm that you've read, understand, and accept", owners: 'Owners', }, + ownershipInfoStep: { + ownerInfo: 'Owner info', + businessOwner: 'Business owner', + signerInfo: 'Signer info', + doYouOwn: ({companyName}: CompanyNameParams) => `Do you own 25% or more of ${companyName}`, + doesAnyoneOwn: ({companyName}: CompanyNameParams) => `Does any individuals own 25% or more of ${companyName}`, + regulationsRequire: 'Regulations require us to verify the identity of any individual who owns more than 25% of the business.', + legalFirstName: 'Legal first name', + legalLastName: 'Legal last name', + whatsTheOwnersName: "What's the owner's legal name?", + whatsYourName: "What's your legal name?", + whatPercentage: 'What percentage of the business belongs to the owner?', + whatsYoursPercentage: 'What percentage of the business do you own?', + ownership: 'Ownership', + whatsTheOwnersDOB: "What's the owner's date of birth?", + whatsYourDOB: "What's your date of birth?", + whatsTheOwnersAddress: "What's the owner's address?", + whatsYourAddress: "What's your address?", + whatAreTheLast: "What are the last 4 digits of the owner's Social Security Number?", + whatsYourLast: 'What are the last 4 digits of your Social Security Number?', + dontWorry: "Don't worry, we don't do any personal credit checks!", + last4: 'Last 4 of SSN', + whyDoWeAsk: 'Why do we ask for this?', + letsDoubleCheck: 'Let’s double check that everything looks right.', + legalName: 'Legal name', + ownershipPercentage: 'Ownership percentage', + areThereOther: ({companyName}: CompanyNameParams) => `Are there other individuals who own 25% or more of ${companyName}`, + owners: 'Owners', + addCertified: 'Add a certified org chart that shows the beneficial owners', + regulationRequiresChart: 'Regulation requires us to collect a certified copy of the ownership chart that shows every individual or entity who owns 25% or more of the business.', + uploadEntity: 'Upload entity ownership chart', + noteEntity: 'Note: Entity ownership chart must be signed by your accountant, legal counsel, or notarized.', + certified: 'Certified entity ownership chart', + selectCountry: 'Select country', + findCountry: 'Find country', + address: 'Address', + }, validationStep: { headerTitle: 'Validate bank account', buttonText: 'Finish setup', diff --git a/src/languages/es.ts b/src/languages/es.ts index c4664386a19e..91557c9defbf 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1976,6 +1976,7 @@ const translations = { noDefaultDepositAccountOrDebitCardAvailable: 'Por favor, añade una cuenta bancaria para depósitos o una tarjeta de débito.', validationAmounts: 'Los importes de validación que introduciste son incorrectos. Por favor, comprueba tu cuenta bancaria e inténtalo de nuevo.', fullName: 'Please enter a valid full name.', + ownershipPercentage: 'Por favor, ingrese un número de porcentaje válido.', }, }, addPersonalBankAccountPage: { @@ -2255,6 +2256,43 @@ const translations = { byAddingThisBankAccount: 'Al añadir esta cuenta bancaria, confirmas que has leído, comprendido y aceptado', owners: 'Dueños', }, + ownershipInfoStep: { + ownerInfo: 'Información del propietario', + businessOwner: 'Propietario del negocio', + signerInfo: 'Información del firmante', + doYouOwn: ({companyName}: CompanyNameParams) => `¿Posee el 25% o más de ${companyName}?`, + doesAnyoneOwn: ({companyName}: CompanyNameParams) => `¿Alguien posee el 25% o más de ${companyName}?`, + regulationsRequire: 'Las regulaciones requieren que verifiquemos la identidad de cualquier persona que posea más del 25% del negocio.', + legalFirstName: 'Nombre legal', + legalLastName: 'Apellido legal', + whatsTheOwnersName: '¿Cuál es el nombre legal del propietario?', + whatsYourName: '¿Cuál es su nombre legal?', + whatPercentage: '¿Qué porcentaje del negocio pertenece al propietario?', + whatsYoursPercentage: '¿Qué porcentaje del negocio posee?', + ownership: 'Propiedad', + whatsTheOwnersDOB: '¿Cuál es la fecha de nacimiento del propietario?', + whatsYourDOB: '¿Cuál es su fecha de nacimiento?', + whatsTheOwnersAddress: '¿Cuál es la dirección del propietario?', + whatsYourAddress: '¿Cuál es su dirección?', + whatAreTheLast: '¿Cuáles son los últimos 4 dígitos del número de seguro social del propietario?', + whatsYourLast: '¿Cuáles son los últimos 4 dígitos de su número de seguro social?', + dontWorry: 'No se preocupe, ¡no realizamos ninguna verificación de crédito personal!', + last4: 'Últimos 4 del SSN', + whyDoWeAsk: '¿Por qué solicitamos esto?', + letsDoubleCheck: 'Verifiquemos que todo esté correcto.', + legalName: 'Nombre legal', + ownershipPercentage: 'Porcentaje de propiedad', + areThereOther: ({companyName}: CompanyNameParams) => `¿Hay otras personas que posean el 25% o más de ${companyName}?`, + owners: 'Propietarios', + addCertified: 'Agregue un organigrama certificado que muestre los propietarios beneficiarios', + regulationRequiresChart: 'La regulación nos exige recopilar una copia certificada del organigrama que muestre a cada persona o entidad que posea el 25% o más del negocio.', + uploadEntity: 'Subir organigrama de propiedad de la entidad', + noteEntity: 'Nota: El organigrama de propiedad de la entidad debe estar firmado por su contador, asesor legal o notariado.', + certified: 'Organigrama certificado de propiedad de la entidad', + selectCountry: 'Seleccionar país', + findCountry: 'Buscar país', + address: 'Dirección', + }, validationStep: { headerTitle: 'Validar cuenta bancaria', buttonText: 'Finalizar configuración', diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 0367325db6b1..f42c0252644d 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -529,6 +529,31 @@ function isValidZipCodeInternational(zipCode: string): boolean { return /^[a-z0-9][a-z0-9\- ]{0,10}[a-z0-9]$/.test(zipCode); } +/** + * Validates the given value if it is correct ownership percentage + * @param value + * @param totalOwnedPercentage + * @param ownerBeingModifiedID + */ +function isValidOwnershipPercentage(value: string, totalOwnedPercentage: Record, ownerBeingModifiedID: string): boolean { + const parsedValue = Number(value); + const isValidNumber = !Number.isNaN(parsedValue) && parsedValue >= 25 && parsedValue <= 100; + + let totalOwnedPercentageSum = 0; + const totalOwnedPercentageKeys = Object.keys(totalOwnedPercentage); + totalOwnedPercentageKeys.forEach((key) => { + if (key === ownerBeingModifiedID) { + return; + } + + totalOwnedPercentageSum += totalOwnedPercentage[key]; + }); + + const isTotalSumValid = totalOwnedPercentageSum + parsedValue <= 100; + + return isValidNumber && isTotalSumValid; +} + export { meetsMinimumAgeRequirement, meetsMaximumAgeRequirement, @@ -576,4 +601,5 @@ export { isValidEmail, isValidPhoneInternational, isValidZipCodeInternational, + isValidOwnershipPercentage, }; diff --git a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO.tsx b/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO.tsx deleted file mode 100644 index 478642416e30..000000000000 --- a/src/pages/ReimbursementAccount/BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import YesNoStep from '@components/SubStepForms/YesNoStep'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; - -type BeneficialOwnerCheckUBOProps = { - /** The title of the question */ - title: string; - - /** The default value of the radio button */ - defaultValue: boolean; - - /** Callback when the value is selected */ - onSelectedValue: (value: boolean) => void; -}; - -function BeneficialOwnerCheckUBO({title, onSelectedValue, defaultValue}: BeneficialOwnerCheckUBOProps) { - const {translate} = useLocalize(); - const styles = useThemeStyles(); - - return ( - - ); -} - -BeneficialOwnerCheckUBO.displayName = 'BeneficialOwnerCheckUBO'; - -export default BeneficialOwnerCheckUBO; diff --git a/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx b/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx index 6d70e966fa2d..6fd3bcb09f80 100644 --- a/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx +++ b/src/pages/ReimbursementAccount/BeneficialOwnersStep.tsx @@ -2,14 +2,15 @@ import {Str} from 'expensify-common'; import React, {useState} from 'react'; import {useOnyx} from 'react-native-onyx'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import YesNoStep from '@components/SubStepForms/YesNoStep'; import useLocalize from '@hooks/useLocalize'; import useSubStep from '@hooks/useSubStep'; import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; import * as BankAccounts from '@userActions/BankAccounts'; import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import BeneficialOwnerCheckUBO from './BeneficialOwnerInfo/substeps/BeneficialOwnerCheckUBO'; import AddressUBO from './BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/AddressUBO'; import ConfirmationUBO from './BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/ConfirmationUBO'; import DateOfBirthUBO from './BeneficialOwnerInfo/substeps/BeneficialOwnerDetailsFormSubsteps/DateOfBirthUBO'; @@ -30,6 +31,7 @@ const bodyContent: Array> = [Le function BeneficialOwnersStep({onBackButtonPress}: BeneficialOwnersStepProps) { const {translate} = useLocalize(); + const styles = useThemeStyles(); const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); @@ -214,16 +216,20 @@ function BeneficialOwnersStep({onBackButtonPress}: BeneficialOwnersStepProps) { stepNames={CONST.BANK_ACCOUNT.STEP_NAMES} > {currentUBOSubstep === SUBSTEP.IS_USER_UBO && ( - )} {currentUBOSubstep === SUBSTEP.IS_ANYONE_ELSE_UBO && ( - @@ -240,8 +246,10 @@ function BeneficialOwnersStep({onBackButtonPress}: BeneficialOwnersStepProps) { )} {currentUBOSubstep === SUBSTEP.ARE_THERE_MORE_UBOS && ( - diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx new file mode 100644 index 000000000000..1629b90a5308 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx @@ -0,0 +1,88 @@ +import React, {useMemo, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import AddressStep from '@components/SubStepForms/AddressStep'; +import useLocalize from '@hooks/useLocalize'; +import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import CONST from '@src/CONST'; +import type {Country} from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type NameProps = SubStepProps & {isUserEnteringHisOwnData: boolean; ownerBeingModifiedID: string}; + +const {STREET, CITY, STATE, ZIP_CODE, COUNTRY, PREFIX} = CONST.NON_USD_BANK_ACCOUNT.BENEFICIAL_OWNER_INFO_STEP.BENEFICIAL_OWNER_DATA; + +function Address({onNext, isEditing, onMove, isUserEnteringHisOwnData, ownerBeingModifiedID}: NameProps) { + const {translate} = useLocalize(); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + + const countryInputKey: `beneficialOwner_${string}_${string}` = `${PREFIX}_${ownerBeingModifiedID}_${COUNTRY}`; + const inputKeys = { + street: `${PREFIX}_${ownerBeingModifiedID}_${STREET}`, + city: `${PREFIX}_${ownerBeingModifiedID}_${CITY}`, + state: `${PREFIX}_${ownerBeingModifiedID}_${STATE}`, + zipCode: `${PREFIX}_${ownerBeingModifiedID}_${ZIP_CODE}`, + country: countryInputKey, + } as const; + + const defaultValues = { + street: reimbursementAccountDraft?.[inputKeys.street] ?? '', + city: reimbursementAccountDraft?.[inputKeys.city] ?? '', + state: reimbursementAccountDraft?.[inputKeys.state] ?? '', + zipCode: reimbursementAccountDraft?.[inputKeys.zipCode] ?? '', + country: (reimbursementAccountDraft?.[inputKeys.country] ?? '') as Country | '', + }; + + const formTitle = translate(isUserEnteringHisOwnData ? 'ownershipInfoStep.whatsYourAddress' : 'ownershipInfoStep.whatsTheOwnersAddress'); + + // Has to be stored in state and updated on country change due to the fact that we can't relay on onyxValues when user is editing the form (draft values are not being saved in that case) + const [shouldDisplayStateSelector, setShouldDisplayStateSelector] = useState( + defaultValues.country === CONST.COUNTRY.US || defaultValues.country === CONST.COUNTRY.CA || defaultValues.country === '', + ); + + const stepFieldsWithState = useMemo( + () => [inputKeys.street, inputKeys.city, inputKeys.state, inputKeys.zipCode, countryInputKey], + [countryInputKey, inputKeys.city, inputKeys.state, inputKeys.street, inputKeys.zipCode], + ); + const stepFieldsWithoutState = useMemo( + () => [inputKeys.street, inputKeys.city, inputKeys.zipCode, countryInputKey], + [countryInputKey, inputKeys.city, inputKeys.street, inputKeys.zipCode], + ); + + const stepFields = shouldDisplayStateSelector ? stepFieldsWithState : stepFieldsWithoutState; + + const handleCountryChange = (country: unknown) => { + if (typeof country !== 'string' || country === '') { + return; + } + setShouldDisplayStateSelector(country === CONST.COUNTRY.US || country === CONST.COUNTRY.CA); + }; + + const handleSubmit = useReimbursementAccountStepFormSubmit({ + fieldIds: stepFields, + onNext, + shouldSaveDraft: isEditing, + }); + + return ( + + isEditing={isEditing} + onNext={onNext} + onMove={onMove} + formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM} + formTitle={formTitle} + formPOBoxDisclaimer={translate('common.noPO')} + onSubmit={handleSubmit} + stepFields={stepFields} + inputFieldsIDs={inputKeys} + defaultValues={defaultValues} + onCountryChange={handleCountryChange} + shouldDisplayStateSelector={shouldDisplayStateSelector} + shouldDisplayCountrySelector + /> + ); +} + +Address.displayName = 'Address'; + +export default Address; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx new file mode 100644 index 000000000000..6b880e8b3ad1 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx @@ -0,0 +1,89 @@ +import React 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 getValuesForBeneficialOwner from '@pages/ReimbursementAccount/NonUSD/utils/getValuesForBeneficialOwner'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type ConfirmationProps = SubStepProps & {ownerBeingModifiedID: string}; + +function Confirmation({onNext, onMove, ownerBeingModifiedID}: ConfirmationProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + const values = getValuesForBeneficialOwner(ownerBeingModifiedID, reimbursementAccountDraft); + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + + {translate('ownershipInfoStep.letsDoubleCheck')} + { + onMove(0); + }} + /> + { + onMove(1); + }} + /> + { + onMove(2); + }} + /> + { + onMove(4); + }} + /> + { + onMove(3); + }} + /> + +