From 471adcfde7157bf3f77dad8537a42c6030c6e98b Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Fri, 8 Nov 2024 10:26:13 +0100 Subject: [PATCH 01/11] feat: Step 4 UI --- src/CONST.ts | 24 ++ src/languages/en.ts | 39 +++ src/languages/es.ts | 39 +++ src/languages/params.ts | 5 + src/libs/ValidationUtils.ts | 26 ++ .../BeneficialOwnerCheck.tsx | 65 ++++ .../Address.tsx | 88 ++++++ .../Confirmation.tsx | 89 ++++++ .../DateOfBirth.tsx | 46 +++ .../Last4SSN.tsx | 67 ++++ .../Name.tsx | 52 +++ .../OwnershipPercentage.tsx | 73 +++++ .../BeneficialOwnerInfo.tsx | 295 +++++++++++++++++- .../BeneficialOwnersList.tsx | 115 +++++++ .../UploadOwnershipChart.tsx | 87 ++++++ .../ReimbursementAccount/NonUSD/WhyLink.tsx | 44 +++ .../utils/getValuesForBeneficialOwner.ts | 63 ++++ src/types/form/ReimbursementAccountForm.ts | 17 + src/types/onyx/ReimbursementAccount.ts | 13 + 19 files changed, 1232 insertions(+), 15 deletions(-) create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Address.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Confirmation.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/DateOfBirth.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Last4SSN.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/Name.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerDetailsFormSubSteps/OwnershipPercentage.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnersList.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/UploadOwnershipChart.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/WhyLink.tsx create mode 100644 src/pages/ReimbursementAccount/NonUSD/utils/getValuesForBeneficialOwner.ts diff --git a/src/CONST.ts b/src/CONST.ts index 23a220e88ddb..0b618a67c72b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -612,6 +612,30 @@ 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', + }, + }, STEP_NAMES: ['1', '2', '3', '4', '5', '6'], STEP_HEADER_HEIGHT: 40, }, diff --git a/src/languages/en.ts b/src/languages/en.ts index b45243b65af5..5111b691254e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -41,6 +41,7 @@ import type { CharacterLimitParams, CompanyCardBankName, CompanyCardFeedNameParams, + CompanyNameParams, ConfirmThatParams, ConnectionNameParams, ConnectionParams, @@ -1944,6 +1945,7 @@ const translations = { lastName: 'Please enter a valid last name.', 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.', + ownershipPercentage: 'Please enter a valid percentage number that is greater than 25', }, }, addPersonalBankAccountPage: { @@ -2219,6 +2221,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 31e713582168..449c7a04d719 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -39,6 +39,7 @@ import type { CharacterLimitParams, CompanyCardBankName, CompanyCardFeedNameParams, + CompanyNameParams, ConfirmThatParams, ConnectionNameParams, ConnectionParams, @@ -1964,6 +1965,7 @@ const translations = { lastName: 'Por favor, introduce los apellidos.', 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.', + ownershipPercentage: 'Por favor, ingrese un número de porcentaje válido que sea mayor a 25', }, }, addPersonalBankAccountPage: { @@ -2242,6 +2244,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/languages/params.ts b/src/languages/params.ts index 2d60c13c4dd0..c40bc01447c3 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -551,6 +551,10 @@ type CurrencyCodeParams = { currencyCode: string; }; +type CompanyNameParams = { + companyName: string; +}; + export type { AuthenticationErrorParams, ImportMembersSuccessfullDescriptionParams, @@ -751,4 +755,5 @@ export type { AssignCardParams, ImportedTypesParams, CurrencyCodeParams, + CompanyNameParams, }; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 47e44bc049d2..e5ec86d03ba7 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -530,6 +530,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, @@ -577,4 +602,5 @@ export { isValidEmail, isValidPhoneInternational, isValidZipCodeInternational, + isValidOwnershipPercentage, }; diff --git a/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx new file mode 100644 index 000000000000..4d108de6dae1 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BeneficialOwnerInfo/BeneficialOwnerCheck.tsx @@ -0,0 +1,65 @@ +import React, {useMemo, useState} from 'react'; +import FormProvider from '@components/Form/FormProvider'; +import type {Choice} from '@components/RadioButtons'; +import RadioButtons from '@components/RadioButtons'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type BeneficialOwnerCheckProps = { + /** 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 BeneficialOwnerCheck({title, onSelectedValue, defaultValue}: BeneficialOwnerCheckProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [value, setValue] = useState(defaultValue); + + const handleSubmit = () => { + onSelectedValue(value); + }; + const handleSelectValue = (newValue: string) => setValue(newValue === 'true'); + const options = useMemo( + () => [ + { + label: translate('common.yes'), + value: 'true', + }, + { + label: translate('common.no'), + value: 'false', + }, + ], + [translate], + ); + + return ( + + {title} + {translate('ownershipInfoStep.regulationsRequire')} + + + ); +} + +BeneficialOwnerCheck.displayName = 'BeneficialOwnerCheck'; + +export default BeneficialOwnerCheck; 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); + }} + /> + +