Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Step 4 UI #52384

Merged
merged 18 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,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,
},
Expand Down
39 changes: 39 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type {
CharacterLimitParams,
CompanyCardBankName,
CompanyCardFeedNameParams,
CompanyNameParams,
ConfirmThatParams,
ConnectionNameParams,
ConnectionParams,
Expand Down Expand Up @@ -1948,6 +1949,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.',
},
},
addPersonalBankAccountPage: {
Expand Down Expand Up @@ -2223,6 +2225,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',
Expand Down
39 changes: 39 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
CharacterLimitParams,
CompanyCardBankName,
CompanyCardFeedNameParams,
CompanyNameParams,
ConfirmThatParams,
ConnectionNameParams,
ConnectionParams,
Expand Down Expand Up @@ -1968,6 +1969,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.',
},
},
addPersonalBankAccountPage: {
Expand Down Expand Up @@ -2246,6 +2248,43 @@ const translations = {
byAddingThisBankAccount: 'Al añadir esta cuenta bancaria, confirmas que has leído, comprendido y aceptado',
owners: 'Dueños',
},
ownershipInfoStep: {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://expensify.slack.com/archives/C01GTK53T8Q/p1731423389064609
Getting a confirmation for the translation

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',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,10 @@ type CurrencyCodeParams = {
currencyCode: string;
};

type CompanyNameParams = {
companyName: string;
};

export type {
AuthenticationErrorParams,
ImportMembersSuccessfullDescriptionParams,
Expand Down Expand Up @@ -751,4 +755,5 @@ export type {
AssignCardParams,
ImportedTypesParams,
CurrencyCodeParams,
CompanyNameParams,
};
26 changes: 26 additions & 0 deletions src/libs/ValidationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, number>, 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,
Expand Down Expand Up @@ -576,4 +601,5 @@ export {
isValidEmail,
isValidPhoneInternational,
isValidZipCodeInternational,
isValidOwnershipPercentage,
};
Original file line number Diff line number Diff line change
@@ -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<Choice[]>(
() => [
{
label: translate('common.yes'),
value: 'true',
},
{
label: translate('common.no'),
value: 'false',
},
],
[translate],
);

return (
<FormProvider
formID={ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM}
submitButtonText={translate('common.confirm')}
onSubmit={handleSubmit}
style={[styles.mh5, styles.flexGrow1]}
>
<Text style={[styles.textHeadlineLineHeightXXL]}>{title}</Text>
<Text style={[styles.pv3, styles.textSupporting]}>{translate('ownershipInfoStep.regulationsRequire')}</Text>
<RadioButtons
items={options}
onPress={handleSelectValue}
defaultCheckedValue={defaultValue.toString()}
radioButtonStyle={[styles.mb6]}
/>
</FormProvider>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a common component called YesNoStep for step like this, can you reuse it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've replaced my component with this step and refactored other places as well - I believe that extra wrapper was unecessary. Ive used YesNoStep directly in many place and cut out the unnecessary components (they were only passing props)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, let me test

);
}

BeneficialOwnerCheck.displayName = 'BeneficialOwnerCheck';

export default BeneficialOwnerCheck;
Original file line number Diff line number Diff line change
@@ -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<boolean>(
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 (
<AddressStep<typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM>
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;
Loading
Loading