diff --git a/package-lock.json b/package-lock.json index d0dddd52d156..9b3f6fc19866 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,7 +51,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.103", + "expensify-common": "2.0.106", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", @@ -24154,9 +24154,9 @@ } }, "node_modules/expensify-common": { - "version": "2.0.103", - "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.103.tgz", - "integrity": "sha512-Q42bUK6TeB87qN4MEBDlhNH1qQqUXY+tJKCZTt01Zv+lcn7KemudOCt7GNoEwfR7LLWsWuec7Vb5x45rQJNC2A==", + "version": "2.0.106", + "resolved": "https://registry.npmjs.org/expensify-common/-/expensify-common-2.0.106.tgz", + "integrity": "sha512-KmxKvglbIUJb0sAcmNxb/AXYAqa3GIZfu3MbmtlYDNJx24mjDjtbGkKhm+16TICDoPj2PDRNogIqgUGWmSSZFQ==", "license": "MIT", "dependencies": { "awesome-phonenumber": "^5.4.0", diff --git a/package.json b/package.json index 61e84e10e4c5..77b312faf0f2 100644 --- a/package.json +++ b/package.json @@ -108,7 +108,7 @@ "date-fns-tz": "^3.2.0", "dom-serializer": "^0.2.2", "domhandler": "^4.3.0", - "expensify-common": "2.0.103", + "expensify-common": "2.0.106", "expo": "51.0.31", "expo-av": "14.0.7", "expo-image": "1.12.15", diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 9c72b371c40f..f4067d357c9d 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -63,6 +63,7 @@ function AddressSearch( onBlur, onInputChange, onPress, + onCountryChange, predefinedPlaces = [], preferredLocale, renamedInputKeys = { @@ -195,7 +196,7 @@ function AddressSearch( // If the address is not in the US, use the full length state name since we're displaying the address's // state / province in a TextInput instead of in a picker. - if (country !== CONST.COUNTRY.US) { + if (country !== CONST.COUNTRY.US && country !== CONST.COUNTRY.CA) { values.state = longStateName; } @@ -244,6 +245,7 @@ function AddressSearch( onInputChange?.(values); } + onCountryChange?.(values.country); onPress?.(values); }; diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index b654fcad99da..daa28c3d69af 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -87,6 +87,9 @@ type AddressSearchProps = { /** The user's preferred locale e.g. 'en', 'es-ES' */ preferredLocale?: Locale; + + /** Callback to be called when the country is changed */ + onCountryChange?: (country: unknown) => void; }; type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent, containerRef: RefObject) => boolean; diff --git a/src/components/CountryPicker/CountrySelectorModal.tsx b/src/components/CountryPicker/CountrySelectorModal.tsx index 76fd53138019..2daa74dcb4e8 100644 --- a/src/components/CountryPicker/CountrySelectorModal.tsx +++ b/src/components/CountryPicker/CountrySelectorModal.tsx @@ -7,8 +7,8 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import searchCountryOptions from '@libs/searchCountryOptions'; -import type {CountryData} from '@libs/searchCountryOptions'; +import searchOptions from '@libs/searchOptions'; +import type {Option} from '@libs/searchOptions'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -27,7 +27,7 @@ type CountrySelectorModalProps = { currentCountry: string; /** Function to call when the user selects a country */ - onCountrySelected: (value: CountryData) => void; + onCountrySelected: (value: Option) => void; /** Function to call when the user presses on the modal backdrop */ onBackdropPress?: () => void; @@ -52,7 +52,7 @@ function CountrySelectorModal({isVisible, currentCountry, onCountrySelected, onC [translate, currentCountry], ); - const searchResults = searchCountryOptions(debouncedSearchValue, countries); + const searchResults = searchOptions(debouncedSearchValue, countries); const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : ''; const styles = useThemeStyles(); diff --git a/src/components/CountryPicker/index.tsx b/src/components/CountryPicker/index.tsx index cc51b3c5f537..3f30fcbafb75 100644 --- a/src/components/CountryPicker/index.tsx +++ b/src/components/CountryPicker/index.tsx @@ -2,7 +2,7 @@ import React, {useState} from 'react'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; -import type {CountryData} from '@libs/searchCountryOptions'; +import type {Option} from '@libs/searchOptions'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import CountrySelectorModal from './CountrySelectorModal'; @@ -26,7 +26,7 @@ function CountryPicker({value, errorText, onInputChange = () => {}}: CountryPick setIsPickerVisible(false); }; - const updateInput = (item: CountryData) => { + const updateInput = (item: Option) => { onInputChange?.(item.value); hidePickerModal(); }; diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 1d66953c1070..5c5c28b82fb9 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -12,6 +12,7 @@ import CONST from '@src/CONST'; import type {OnyxFormDraftKey, OnyxFormKey} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Form} from '@src/types/form'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {RegisterInput} from './FormContext'; import FormContext from './FormContext'; @@ -244,9 +245,20 @@ function FormProvider( setErrors({}); }, [formID]); + const resetFormFieldError = useCallback( + (inputID: keyof Form) => { + const newErrors = {...errors}; + delete newErrors[inputID]; + FormActions.setErrors(formID, newErrors as Errors); + setErrors(newErrors); + }, + [errors, formID], + ); + useImperativeHandle(forwardedRef, () => ({ resetForm, resetErrors, + resetFormFieldError, })); const registerInput = useCallback( diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index a77fabc52ce9..ab9260a6b5d9 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -33,6 +33,7 @@ import type NetSuiteCustomListPicker from '@pages/workspace/accounting/netsuite/ import type NetSuiteMenuWithTopDescriptionForm from '@pages/workspace/accounting/netsuite/import/NetSuiteImportCustomFieldNew/NetSuiteMenuWithTopDescriptionForm'; import type {Country} from '@src/CONST'; import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS'; +import type {Form} from '@src/types/form'; import type {BaseForm} from '@src/types/form/Form'; /** @@ -164,6 +165,7 @@ type FormProps = { type FormRef = { resetForm: (optionalValue: FormOnyxValues) => void; resetErrors: () => void; + resetFormFieldError: (fieldID: keyof Form) => void; }; type InputRefs = Record>; diff --git a/src/components/PushRowWithModal/PushRowModal.tsx b/src/components/PushRowWithModal/PushRowModal.tsx index 79fbc53c1e2c..aa9fa0538dff 100644 --- a/src/components/PushRowWithModal/PushRowModal.tsx +++ b/src/components/PushRowWithModal/PushRowModal.tsx @@ -1,10 +1,13 @@ -import React, {useEffect, useState} from 'react'; +import React, {useMemo} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; +import useDebouncedState from '@hooks/useDebouncedState'; import useLocalize from '@hooks/useLocalize'; +import searchOptions from '@libs/searchOptions'; +import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; type PushRowModalProps = { @@ -40,44 +43,28 @@ type ListItemType = { function PushRowModal({isVisible, selectedOption, onOptionChange, onClose, optionsList, headerTitle, searchInputTitle}: PushRowModalProps) { const {translate} = useLocalize(); - const allOptions = Object.entries(optionsList).map(([key, value]) => ({ - value: key, - text: value, - keyForList: key, - isSelected: key === selectedOption, - })); - const [searchbarInputText, setSearchbarInputText] = useState(''); - const [optionListItems, setOptionListItems] = useState(allOptions); - - useEffect(() => { - setOptionListItems((prevOptionListItems) => - prevOptionListItems.map((option) => ({ - ...option, - isSelected: option.value === selectedOption, + const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); + + const options = useMemo( + () => + Object.entries(optionsList).map(([key, value]) => ({ + value: key, + text: value, + keyForList: key, + isSelected: key === selectedOption, + searchValue: StringUtils.sanitizeString(value), })), - ); - }, [selectedOption]); - - const filterShownOptions = (searchText: string) => { - setSearchbarInputText(searchText); - const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) ?? []; - setOptionListItems( - allOptions.filter((option) => - searchWords.every((word) => - option.text - .toLowerCase() - .replace(/[^a-z0-9]/g, ' ') - .includes(word), - ), - ), - ); - }; + [optionsList, selectedOption], + ); const handleSelectRow = (option: ListItemType) => { onOptionChange(option.value); onClose(); }; + const searchResults = searchOptions(debouncedSearchValue, options); + const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : ''; + return ( option.value === selectedOption)?.keyForList} + sections={[{data: searchResults}]} + initiallyFocusedOptionKey={selectedOption} showScrollIndicator shouldShowTooltips={false} ListItem={RadioListItem} diff --git a/src/components/PushRowWithModal/index.tsx b/src/components/PushRowWithModal/index.tsx index 65c1969fdf8b..83128899b50f 100644 --- a/src/components/PushRowWithModal/index.tsx +++ b/src/components/PushRowWithModal/index.tsx @@ -8,11 +8,11 @@ type PushRowWithModalProps = { /** The list of options that we want to display where key is option code and value is option name */ optionsList: Record; - /** The currently selected option */ - selectedOption: string; + /** Current value of the selected item */ + value?: string; - /** Function to call when the user selects an option */ - onOptionChange: (option: string) => void; + /** Function called whenever list item is selected */ + onInputChange?: (value: string, key?: string) => void; /** Additional styles to apply to container */ wrapperStyles?: StyleProp; @@ -32,13 +32,12 @@ type PushRowWithModalProps = { /** Text to display on error message */ errorText?: string; - /** Function called whenever option changes */ - onInputChange?: (value: string) => void; + /** The ID of the input that should be reset when the value changes */ + stateInputIDToReset?: string; }; function PushRowWithModal({ - selectedOption, - onOptionChange, + value, optionsList, wrapperStyles, description, @@ -47,6 +46,7 @@ function PushRowWithModal({ shouldAllowChange = true, errorText, onInputChange = () => {}, + stateInputIDToReset, }: PushRowWithModalProps) { const [isModalVisible, setIsModalVisible] = useState(false); @@ -58,16 +58,19 @@ function PushRowWithModal({ setIsModalVisible(true); }; - const handleOptionChange = (value: string) => { - onOptionChange(value); - onInputChange(value); + const handleOptionChange = (optionValue: string) => { + onInputChange(optionValue); + + if (stateInputIDToReset) { + onInputChange('', stateInputIDToReset); + } }; return ( <> void; + onStateSelected: (value: Option) => void; /** Function to call when the user presses on the modal backdrop */ onBackdropPress?: () => void; @@ -56,7 +56,7 @@ function StateSelectorModal({isVisible, currentState, onStateSelected, onClose, [translate, currentState], ); - const searchResults = searchCountryOptions(debouncedSearchValue, countryStates); + const searchResults = searchOptions(debouncedSearchValue, countryStates); const headerMessage = debouncedSearchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : ''; const styles = useThemeStyles(); diff --git a/src/components/StatePicker/index.tsx b/src/components/StatePicker/index.tsx index ebcb156fd293..558db66a52ec 100644 --- a/src/components/StatePicker/index.tsx +++ b/src/components/StatePicker/index.tsx @@ -3,7 +3,7 @@ import React, {useState} from 'react'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; -import type {CountryData} from '@libs/searchCountryOptions'; +import type {Option} from '@libs/searchOptions'; import CONST from '@src/CONST'; import StateSelectorModal from './StateSelectorModal'; @@ -28,7 +28,7 @@ function StatePicker({value, errorText, onInputChange = () => {}}: StatePickerPr setIsPickerVisible(false); }; - const updateInput = (item: CountryData) => { + const updateInput = (item: Option) => { onInputChange?.(item.value); hidePickerModal(); }; diff --git a/src/components/SubStepForms/AddressStep.tsx b/src/components/SubStepForms/AddressStep.tsx index 9a90d2fa7a42..86e6e328d226 100644 --- a/src/components/SubStepForms/AddressStep.tsx +++ b/src/components/SubStepForms/AddressStep.tsx @@ -1,7 +1,7 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import FormProvider from '@components/Form/FormProvider'; -import type {FormInputErrors, FormOnyxKeys, FormOnyxValues, FormValue} from '@components/Form/types'; +import type {FormInputErrors, FormOnyxKeys, FormOnyxValues, FormRef, FormValue} from '@components/Form/types'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import type {SubStepProps} from '@hooks/useSubStep/types'; @@ -37,7 +37,7 @@ type AddressStepProps = SubStepProp /** Fields list of the form */ stepFields: Array>; - /* The IDs of the input fields */ + /** The IDs of the input fields */ inputFieldsIDs: AddressValues; /** The default values for the form */ @@ -45,6 +45,24 @@ type AddressStepProps = SubStepProp /** Should show help links */ shouldShowHelpLinks?: boolean; + + /** Indicates if country selector should be displayed */ + shouldDisplayCountrySelector?: boolean; + + /** Indicates if state selector should be displayed */ + shouldDisplayStateSelector?: boolean; + + /** Label for the state selector */ + stateSelectorLabel?: string; + + /** The title of the state selector modal */ + stateSelectorModalHeaderTitle?: string; + + /** The title of the state selector search input */ + stateSelectorSearchInputTitle?: string; + + /** Callback to be called when the country is changed */ + onCountryChange?: (country: unknown) => void; }; function AddressStep({ @@ -58,10 +76,23 @@ function AddressStep({ defaultValues, shouldShowHelpLinks, isEditing, + shouldDisplayCountrySelector = false, + shouldDisplayStateSelector = true, + stateSelectorLabel, + stateSelectorModalHeaderTitle, + stateSelectorSearchInputTitle, + onCountryChange, }: AddressStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const formRef = useRef(null); + + useEffect(() => { + // When stepFields change (e.g. country changes) we need to reset state errors manually + formRef.current?.resetFormFieldError(inputFieldsIDs.state); + }, [inputFieldsIDs.state, stepFields]); + const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { const errors = ValidationUtils.getFieldRequiredErrors(values, stepFields); @@ -73,14 +104,14 @@ function AddressStep({ } const zipCode = values[inputFieldsIDs.zipCode as keyof typeof values]; - if (zipCode && !ValidationUtils.isValidZipCode(zipCode as string)) { + if (zipCode && (shouldDisplayCountrySelector ? !ValidationUtils.isValidZipCodeInternational(zipCode as string) : !ValidationUtils.isValidZipCode(zipCode as string))) { // @ts-expect-error type mismatch to be fixed errors[inputFieldsIDs.zipCode] = translate('bankAccount.error.zipCode'); } return errors; }, - [inputFieldsIDs.street, inputFieldsIDs.zipCode, stepFields, translate], + [inputFieldsIDs.street, inputFieldsIDs.zipCode, shouldDisplayCountrySelector, stepFields, translate], ); return ( @@ -90,6 +121,7 @@ function AddressStep({ validate={customValidate ?? validate} onSubmit={onSubmit} style={[styles.mh5, styles.flexGrow1]} + ref={formRef} > {formTitle} @@ -99,6 +131,12 @@ function AddressStep({ streetTranslationKey="common.streetAddress" defaultValues={defaultValues} shouldSaveDraft={!isEditing} + shouldDisplayStateSelector={shouldDisplayStateSelector} + shouldDisplayCountrySelector={shouldDisplayCountrySelector} + stateSelectorLabel={stateSelectorLabel} + stateSelectorModalHeaderTitle={stateSelectorModalHeaderTitle} + stateSelectorSearchInputTitle={stateSelectorSearchInputTitle} + onCountryChange={onCountryChange} /> {!!shouldShowHelpLinks && } diff --git a/src/languages/en.ts b/src/languages/en.ts index 92bed5ecf1f0..f2f1ff06875f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -44,6 +44,7 @@ import type { ConfirmThatParams, ConnectionNameParams, ConnectionParams, + CurrencyCodeParams, CustomersOrJobsLabelParams, DateParams, DateShouldBeAfterParams, @@ -346,6 +347,7 @@ const translations = { pleaseSelectOne: 'Please select an option above.', invalidRateError: 'Please enter a valid rate.', lowRateError: 'Rate must be greater than 0.', + email: 'Please enter a valid email address.', }, comma: 'comma', semicolon: 'semicolon', @@ -2169,6 +2171,33 @@ const translations = { listOfRestrictedBusinesses: 'list of restricted businesses', confirmCompanyIsNot: 'I confirm that this company is not on the', businessInfoTitle: 'Business info', + legalBusinessName: 'Legal business name', + whatsTheBusinessName: "What's the business name?", + whatsTheBusinessAddress: "What's the business address?", + whatsTheBusinessContactInformation: "What's the business contact information?", + whatsTheBusinessRegistrationNumber: "What's the business registration number?", + whatsThisNumber: "What's this number?", + whereWasTheBusinessIncorporated: 'Where was the business incorporated?', + whatTypeOfBusinessIsIt: 'What type of business is it?', + whatsTheBusinessAnnualPayment: "What's the business's annual payment volume?", + registrationNumber: 'Registration number', + businessAddress: 'Business address', + businessType: 'Business type', + incorporation: 'Incorporation', + incorporationCountry: 'Incorporation country', + incorporationTypeName: 'Incorporation type', + businessCategory: 'Business category', + annualPaymentVolume: 'Annual payment volume', + annualPaymentVolumeInCurrency: ({currencyCode}: CurrencyCodeParams) => `Annual payment volume in ${currencyCode}`, + selectIncorporationType: 'Select incorporation type', + selectBusinessCategory: 'Select business category', + selectAnnualPaymentVolume: 'Select annual payment volume', + selectIncorporationCountry: 'Select incorporation country', + selectIncorporationState: 'Select incorporation state', + findIncorporationType: 'Find incorporation type', + findBusinessCategory: 'Find business category', + findAnnualPaymentVolume: 'Find annual payment volume', + findIncorporationState: 'Find incorporation state', }, beneficialOwnerInfoStep: { doYouOwn25percent: 'Do you own 25% or more of', diff --git a/src/languages/es.ts b/src/languages/es.ts index eefbb16870b5..6c10c9990c1f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -42,6 +42,7 @@ import type { ConfirmThatParams, ConnectionNameParams, ConnectionParams, + CurrencyCodeParams, CustomersOrJobsLabelParams, DateParams, DateShouldBeAfterParams, @@ -336,6 +337,7 @@ const translations = { pleaseSelectOne: 'Seleccione una de las opciones.', invalidRateError: 'Por favor, introduce una tarifa válida.', lowRateError: 'La tarifa debe ser mayor que 0.', + email: 'Por favor, introduzca una dirección de correo electrónico válida.', }, comma: 'la coma', semicolon: 'el punto y coma', @@ -2192,6 +2194,33 @@ const translations = { listOfRestrictedBusinesses: 'lista de negocios restringidos', confirmCompanyIsNot: 'Confirmo que esta empresa no está en la', businessInfoTitle: 'Información del negocio', + legalBusinessName: 'Nombre legal de la empresa', + whatsTheBusinessName: '¿Cuál es el nombre de la empresa?', + whatsTheBusinessAddress: '¿Cuál es la dirección de la empresa?', + whatsTheBusinessContactInformation: '¿Cuál es la información de contacto de la empresa?', + whatsTheBusinessRegistrationNumber: '¿Cuál es el número de registro de la empresa?', + whatsThisNumber: '¿Qué es este número?', + whereWasTheBusinessIncorporated: '¿Dónde se constituyó la empresa?', + whatTypeOfBusinessIsIt: '¿Qué tipo de empresa es?', + whatsTheBusinessAnnualPayment: '¿Cuál es el volumen anual de pagos de la empresa?', + registrationNumber: 'Número de registro', + businessAddress: 'Dirección de la empresa', + businessType: 'Tipo de empresa', + incorporation: 'Constitución', + incorporationCountry: 'País de constitución', + incorporationTypeName: 'Tipo de constitución', + businessCategory: 'Categoría de la empresa', + annualPaymentVolume: 'Volumen anual de pagos', + annualPaymentVolumeInCurrency: ({currencyCode}: CurrencyCodeParams) => `Volumen anual de pagos en ${currencyCode}`, + selectIncorporationType: 'Seleccione tipo de constitución', + selectBusinessCategory: 'Seleccione categoría de la empresa', + selectAnnualPaymentVolume: 'Seleccione volumen anual de pagos', + selectIncorporationCountry: 'Seleccione país de constitución', + selectIncorporationState: 'Seleccione estado de constitución', + findIncorporationType: 'Buscar tipo de constitución', + findBusinessCategory: 'Buscar categoría de la empresa', + findAnnualPaymentVolume: 'Buscar volumen anual de pagos', + findIncorporationState: 'Buscar estado de constitución', }, beneficialOwnerInfoStep: { doYouOwn25percent: '¿Posees el 25% o más de', diff --git a/src/languages/params.ts b/src/languages/params.ts index e9f0c4370357..2d60c13c4dd0 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -547,6 +547,10 @@ type CompanyCardBankName = { bankName: string; }; +type CurrencyCodeParams = { + currencyCode: string; +}; + export type { AuthenticationErrorParams, ImportMembersSuccessfullDescriptionParams, @@ -746,4 +750,5 @@ export type { OptionalParam, AssignCardParams, ImportedTypesParams, + CurrencyCodeParams, }; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 664f2416867a..0367325db6b1 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -502,6 +502,33 @@ function isValidSubscriptionSize(subscriptionSize: string): boolean { return !Number.isNaN(parsedSubscriptionSize) && parsedSubscriptionSize > 0 && parsedSubscriptionSize <= CONST.SUBSCRIPTION_SIZE_LIMIT && Number.isInteger(parsedSubscriptionSize); } +/** + * Validates the given value if it is correct email address. + * @param email + */ +function isValidEmail(email: string): boolean { + return Str.isValidEmail(email); +} + +/** + * Validates the given value if it is correct phone number in E164 format (international standard). + * @param phoneNumber + */ +function isValidPhoneInternational(phoneNumber: string): boolean { + const phoneNumberWithCountryCode = LoginUtils.appendCountryCode(phoneNumber); + const parsedPhoneNumber = parsePhoneNumber(phoneNumberWithCountryCode); + + return parsedPhoneNumber.possible && Str.isValidE164Phone(parsedPhoneNumber.number?.e164 ?? ''); +} + +/** + * Validates the given value if it is correct zip code for international addresses. + * @param zipCode + */ +function isValidZipCodeInternational(zipCode: string): boolean { + return /^[a-z0-9][a-z0-9\- ]{0,10}[a-z0-9]$/.test(zipCode); +} + export { meetsMinimumAgeRequirement, meetsMaximumAgeRequirement, @@ -546,4 +573,7 @@ export { isValidSubscriptionSize, isExistingTaxCode, isPublicDomain, + isValidEmail, + isValidPhoneInternational, + isValidZipCodeInternational, }; diff --git a/src/libs/searchCountryOptions.ts b/src/libs/searchOptions.ts similarity index 59% rename from src/libs/searchCountryOptions.ts rename to src/libs/searchOptions.ts index 953a5c81c77f..4c8021dffa10 100644 --- a/src/libs/searchCountryOptions.ts +++ b/src/libs/searchOptions.ts @@ -1,6 +1,6 @@ import StringUtils from './StringUtils'; -type CountryData = { +type Option = { value: string; keyForList: string; text: string; @@ -9,32 +9,32 @@ type CountryData = { }; /** - * Searches the countries/states data and returns sorted results based on the search query - * @param countriesData - An array of country data objects - * @returns An array of countries/states sorted based on the search query + * Searches the options and returns sorted results based on the search query + * @param options - An array of option objects + * @returns An array of options sorted based on the search query */ -function searchCountryOptions(searchValue: string, countriesData: CountryData[]): CountryData[] { +function searchOptions(searchValue: string, options: Option[]): Option[] { if (!searchValue) { - return countriesData; + return options; } const trimmedSearchValue = StringUtils.sanitizeString(searchValue); if (!trimmedSearchValue) { return []; } - const filteredData = countriesData.filter((country) => country.searchValue.includes(trimmedSearchValue)); + const filteredData = options.filter((option) => option.searchValue.includes(trimmedSearchValue)); const halfSorted = filteredData.sort((a, b) => { // Prioritize matches at the beginning of the string // e.g. For the search term "Bar" "Barbados" should be prioritized over Antigua & Barbuda // The first two characters are the country code, so we start at index 2 // and end at the length of the search term - const countryNameASubstring = a.searchValue.toLowerCase().substring(2, trimmedSearchValue.length + 2); - const countryNameBSubstring = b.searchValue.toLowerCase().substring(2, trimmedSearchValue.length + 2); - if (countryNameASubstring === trimmedSearchValue.toLowerCase()) { + const optionASubstring = a.searchValue.toLowerCase().substring(2, trimmedSearchValue.length + 2); + const optionBSubstring = b.searchValue.toLowerCase().substring(2, trimmedSearchValue.length + 2); + if (optionASubstring === trimmedSearchValue.toLowerCase()) { return -1; } - if (countryNameBSubstring === trimmedSearchValue.toLowerCase()) { + if (optionBSubstring === trimmedSearchValue.toLowerCase()) { return 1; } return 0; @@ -46,12 +46,12 @@ function searchCountryOptions(searchValue: string, countriesData: CountryData[]) // Diacritic detected, prioritize diacritic matches // We search for diacritic matches by using the unsanitized country name and search term fullSorted = halfSorted.sort((a, b) => { - const unsanitizedCountryNameA = a.text.toLowerCase(); - const unsanitizedCountryNameB = b.text.toLowerCase(); - if (unsanitizedCountryNameA.includes(unsanitizedSearchValue)) { + const unsanitizedOptionA = a.text.toLowerCase(); + const unsanitizedOptionB = b.text.toLowerCase(); + if (unsanitizedOptionA.includes(unsanitizedSearchValue)) { return -1; } - if (unsanitizedCountryNameB.includes(unsanitizedSearchValue)) { + if (unsanitizedOptionB.includes(unsanitizedSearchValue)) { return 1; } return 0; @@ -72,5 +72,5 @@ function searchCountryOptions(searchValue: string, countriesData: CountryData[]) return fullSorted; } -export default searchCountryOptions; -export type {CountryData}; +export default searchOptions; +export type {Option}; diff --git a/src/pages/ReimbursementAccount/AddressFormFields.tsx b/src/pages/ReimbursementAccount/AddressFormFields.tsx index a863d3cc5952..c095e439cbd8 100644 --- a/src/pages/ReimbursementAccount/AddressFormFields.tsx +++ b/src/pages/ReimbursementAccount/AddressFormFields.tsx @@ -1,9 +1,10 @@ -import React from 'react'; +import {CONST as COMMON_CONST} from 'expensify-common/dist/CONST'; +import React, {useState} from 'react'; import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import AddressSearch from '@components/AddressSearch'; import InputWrapper from '@components/Form/InputWrapper'; -import type {State} from '@components/StateSelector'; -import StateSelector from '@components/StateSelector'; +import PushRowWithModal from '@components/PushRowWithModal'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -17,9 +18,6 @@ type AddressFormProps = { /** Translate key for Street name */ streetTranslationKey: TranslationPaths; - /** Callback fired when a field changes. Passes args as {[fieldName]: val} */ - onFieldChange?: (value: T) => void; - /** Default values */ defaultValues?: Address; @@ -34,14 +32,70 @@ type AddressFormProps = { /** Saves a draft of the input value when used in a form */ shouldSaveDraft?: boolean; + + /** Additional styles to apply to container */ + containerStyles?: StyleProp; + + /** Indicates if country selector should be displayed */ + shouldDisplayCountrySelector?: boolean; + + /** Indicates if state selector should be displayed */ + shouldDisplayStateSelector?: boolean; + + /** Label for the state selector */ + stateSelectorLabel?: string; + + /** The title of the state selector modal */ + stateSelectorModalHeaderTitle?: string; + + /** The title of the state selector search input */ + stateSelectorSearchInputTitle?: string; + + /** Callback to be called when the country is changed */ + onCountryChange?: (country: unknown) => void; }; -function AddressFormFields({shouldSaveDraft = false, defaultValues, values, errors, inputKeys, onFieldChange, streetTranslationKey}: AddressFormProps) { +const PROVINCES_LIST_OPTIONS = (Object.keys(COMMON_CONST.PROVINCES) as Array).reduce((acc, key) => { + acc[COMMON_CONST.PROVINCES[key].provinceISO] = COMMON_CONST.PROVINCES[key].provinceName; + return acc; +}, {} as Record); + +const STATES_LIST_OPTIONS = (Object.keys(COMMON_CONST.STATES) as Array).reduce((acc, key) => { + acc[COMMON_CONST.STATES[key].stateISO] = COMMON_CONST.STATES[key].stateName; + return acc; +}, {} as Record); + +function AddressFormFields({ + shouldSaveDraft = false, + defaultValues, + values, + errors, + inputKeys, + streetTranslationKey, + containerStyles, + shouldDisplayCountrySelector = false, + shouldDisplayStateSelector = true, + stateSelectorLabel, + stateSelectorModalHeaderTitle, + stateSelectorSearchInputTitle, + onCountryChange, +}: AddressFormProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [countryInEditMode, setCountryInEditMode] = useState(defaultValues?.country ?? CONST.COUNTRY.US); + // When draft values are not being saved we need to relay on local state to determine the currently selected country + const currentlySelectedCountry = shouldSaveDraft ? defaultValues?.country : countryInEditMode; + + const handleCountryChange = (country: unknown) => { + if (typeof country === 'string' && country !== '') { + setCountryInEditMode(country); + } + onCountryChange?.(country); + }; + return ( - <> + onFieldChange?.({city: value})} errorText={errors?.city ? translate('bankAccount.error.addressCity') : ''} containerStyles={styles.mt6} /> - - onFieldChange?.({state: value})} - errorText={errors?.state ? translate('bankAccount.error.addressState') : ''} - /> - + {shouldDisplayStateSelector && ( + + + + )} onFieldChange?.({zipCode: value})} errorText={errors?.zipCode ? translate('bankAccount.error.zipCode') : ''} maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE} hint={translate('common.zipCodeExampleFormat', {zipSampleFormat: CONST.COUNTRY_ZIP_REGEX_DATA.US.samples})} containerStyles={styles.mt3} /> - + {shouldDisplayCountrySelector && ( + + + + )} + ); } diff --git a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/BusinessInfo.tsx b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/BusinessInfo.tsx index 8d1781edefbd..61b42789daea 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/BusinessInfo.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/BusinessInfo.tsx @@ -5,7 +5,14 @@ import useLocalize from '@hooks/useLocalize'; import useSubStep from '@hooks/useSubStep'; import type {SubStepProps} from '@hooks/useSubStep/types'; import CONST from '@src/CONST'; +import Address from './substeps/Address'; +import BusinessType from './substeps/BusinessType'; import Confirmation from './substeps/Confirmation'; +import ContactInformation from './substeps/ContactInformation'; +import IncorporationLocation from './substeps/IncorporationLocation'; +import Name from './substeps/Name'; +import PaymentVolume from './substeps/PaymentVolume'; +import RegistrationNumber from './substeps/RegistrationNumber'; type BusinessInfoProps = { /** Handles back button press */ @@ -15,7 +22,7 @@ type BusinessInfoProps = { onSubmit: () => void; }; -const bodyContent: Array> = [Confirmation]; +const bodyContent: Array> = [Name, Address, ContactInformation, RegistrationNumber, IncorporationLocation, BusinessType, PaymentVolume, Confirmation]; function BusinessInfo({onBackButtonPress, onSubmit}: BusinessInfoProps) { const {translate} = useLocalize(); diff --git a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/mockedCorpayLists.ts b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/mockedCorpayLists.ts new file mode 100644 index 000000000000..3acde3dc6577 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/mockedCorpayLists.ts @@ -0,0 +1,413 @@ +// TODO - Remove this file once GetCorpayOnboardingFields method is fully implemented. It should when we start work on https://github.com/Expensify/App/issues/50905 + +const annualVolumeRange = [ + { + id: '0', + name: 'Undefined', + stringValue: 'Undefined', + }, + { + id: '1', + name: 'LessThan25000', + stringValue: 'Less than 25000', + }, + { + id: '2', + name: 'TwentyFiveThousandToFiftyThousand', + stringValue: '25,000 - 50,000', + }, + { + id: '3', + name: 'FiftyThousandToSeventyFiveThousand', + stringValue: '50,000 – 75,000', + }, + { + id: '4', + name: 'SeventyFiveToOneHundredThousand', + stringValue: '75,000 – 100,000', + }, + { + id: '5', + name: 'OneHundredToOneHundredFiftyThousand', + stringValue: '100,000 – 150,000', + }, + { + id: '6', + name: 'OneHundredFiftyToTwoHundredThousand', + stringValue: '150,000 – 200,000', + }, + { + id: '7', + name: 'TwoHundredToTwoHundredFiftyThousand', + stringValue: '200,000 – 250,000', + }, + { + id: '8', + name: 'TwoHundredFiftyToThreeHundredThousand', + stringValue: '250,000 – 300,000', + }, + { + id: '9', + name: 'ThreeHundredToFourHundredThousand', + stringValue: '300,000 – 400,000', + }, + { + id: '10', + name: 'FourHundredToFiveHundredThousand', + stringValue: '400,000 – 500,000', + }, + { + id: '11', + name: 'FiveHundredToSevenHundredFiftyThousand', + stringValue: '500,000 – 750,000', + }, + { + id: '12', + name: 'SevenHundredFiftyThousandToOneMillion', + stringValue: '750,000 – 1 million', + }, + { + id: '13', + name: 'OneMillionToTwoMillion', + stringValue: '1 million – 2 million', + }, + { + id: '14', + name: 'TwoMillionToThreeMillion', + stringValue: '2 million – 3 million', + }, + { + id: '15', + name: 'ThreeMillionToFiveMillion', + stringValue: '3 million – 5 million', + }, + { + id: '16', + name: 'FiveMillionToSevenPointFiveMillion', + stringValue: '5 million – 7.5 million', + }, + { + id: '17', + name: 'SevenPointFiveMillionToTenMillion', + stringValue: '7.5 million – 10 million', + }, + { + id: '18', + name: 'GreaterThan10Million', + stringValue: 'Greater than 10 Million', + }, +]; + +// eslint-disable-next-line rulesdir/no-negated-variables +const applicantType = [ + { + id: '0', + name: 'Undefined', + stringValue: 'Undefined', + }, + { + id: '1', + name: 'Corporation', + stringValue: 'Corporation', + }, + { + id: '2', + name: 'Limited_Liability_Company', + stringValue: 'Limited Liability Company (e.g., LLC, LC)', + }, + { + id: '3', + name: 'Partnership', + stringValue: 'Partnership', + }, + { + id: '4', + name: 'Partnership_UK', + stringValue: 'Partnership UK', + }, + { + id: '5', + name: 'Unincorporated_Entity', + stringValue: 'Unincorporated Entity', + }, + { + id: '6', + name: 'Sole_Proprietorship_Sole_Trader', + stringValue: 'Sole Proprietorship/Sole Trader', + }, + { + id: '7', + name: 'Private_person_Entity', + stringValue: 'Private person/ Entity', + }, + { + id: '8', + name: 'Personal_Account', + stringValue: 'Personal Account', + }, + { + id: '9', + name: 'Financial_Institution', + stringValue: 'Financial Institution', + }, + { + id: '10', + name: 'Non_Profit', + stringValue: 'Not for Profit', + }, + { + id: '11', + name: 'Online_User_Verification', + stringValue: 'Online User Verification', + }, + { + id: '12', + name: 'Charitable_Organization', + stringValue: 'Charitable Organizationt', + }, + { + id: '13', + name: 'Trust', + stringValue: 'Trust', + }, +]; + +const natureOfBusiness = [ + { + id: '0', + name: 'Undefined', + stringValue: 'Undefined', + }, + { + id: '10', + name: 'Aerospace and defense', + stringValue: 'Aerospace and defense', + }, + { + id: '20', + name: 'Agriculture and agric-food', + stringValue: 'Agriculture and agric-food', + }, + { + id: '30', + name: 'Apparel / Clothing', + stringValue: 'Apparel / Clothing', + }, + { + id: '40', + name: 'Automotive / Trucking', + stringValue: 'Automotive / Trucking', + }, + { + id: '50', + name: 'Books / Magazines', + stringValue: 'Books / Magazines', + }, + { + id: '60', + name: 'Broadcasting', + stringValue: 'Broadcasting', + }, + { + id: '70', + name: 'Building products', + stringValue: 'Building products', + }, + { + id: '80', + name: 'Chemicals', + stringValue: 'Chemicals', + }, + { + id: '90', + name: 'Dairy', + stringValue: 'Dairy', + }, + { + id: '100', + name: 'E-business', + stringValue: 'E-business', + }, + { + id: '105', + name: 'Educational Institutes', + stringValue: 'Educational Institutes', + }, + { + id: '110', + name: 'Environment', + stringValue: 'Environment', + }, + { + id: '120', + name: 'Explosives', + stringValue: 'Explosives', + }, + { + id: '140', + name: 'Fisheries and oceans', + stringValue: 'Fisheries and oceans', + }, + { + id: '150', + name: 'Food / Beverage distribution', + stringValue: 'Food / Beverage distribution', + }, + { + id: '160', + name: 'Footwear', + stringValue: 'Footwear', + }, + { + id: '170', + name: 'Forest industries', + stringValue: 'Forest industries', + }, + { + id: '180', + name: 'Furniture', + stringValue: 'Furniture', + }, + { + id: '190', + name: 'Giftware and crafts', + stringValue: 'Giftware and crafts', + }, + { + id: '200', + name: 'Horticulture', + stringValue: 'Horticulture', + }, + { + id: '210', + name: 'Hydroelectric energy', + stringValue: 'Hydroelectric energy', + }, + { + id: '220', + name: 'Information and communication technologies', + stringValue: 'Information and communication technologies', + }, + { + id: '230', + name: 'Intelligent systems', + stringValue: 'Intelligent systems', + }, + { + id: '240', + name: 'Livestock', + stringValue: 'Livestock', + }, + { + id: '250', + name: 'Medical devices', + stringValue: 'Medical devices', + }, + { + id: '251', + name: 'Medical treatment', + stringValue: 'Medical treatment', + }, + { + id: '260', + name: 'Minerals, metals and mining', + stringValue: 'Minerals, metals and mining', + }, + { + id: '270', + name: 'Oil and gas', + stringValue: 'Oil and gas', + }, + { + id: '280', + name: 'Pharmaceuticals and biopharmaceuticals', + stringValue: 'Pharmaceuticals and biopharmaceuticals', + }, + { + id: '290', + name: 'Plastics', + stringValue: 'Plastics', + }, + { + id: '300', + name: 'Poultry and eggs', + stringValue: 'Poultry and eggs', + }, + { + id: '310', + name: 'Printing /Publishing', + stringValue: 'Printing /Publishing', + }, + { + id: '320', + name: 'Product design and development', + stringValue: 'Product design and development', + }, + { + id: '330', + name: 'Railway', + stringValue: 'Railway', + }, + { + id: '340', + name: 'Retail', + stringValue: 'Retail', + }, + { + id: '350', + name: 'Shipping and industrial marine', + stringValue: 'Shipping and industrial marine', + }, + { + id: '360', + name: 'Soil', + stringValue: 'Soil', + }, + { + id: '370', + name: 'Sound recording', + stringValue: 'Sound recording', + }, + { + id: '380', + name: 'Sporting goods', + stringValue: 'Sporting goods', + }, + { + id: '390', + name: 'Telecommunications equipment', + stringValue: 'Telecommunications equipment', + }, + { + id: '400', + name: 'Television', + stringValue: 'Television', + }, + { + id: '410', + name: 'Textiles', + stringValue: 'Textiles', + }, + { + id: '420', + name: 'Tourism', + stringValue: 'Tourism', + }, + { + id: '425', + name: 'Trademarks / Law', + stringValue: 'Trademarks / Law', + }, + { + id: '430', + name: 'Water supply', + stringValue: 'Water supply', + }, + { + id: '440', + name: 'Wholesale', + stringValue: 'Wholesale', + }, +]; + +export {annualVolumeRange, applicantType, natureOfBusiness}; diff --git a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/substeps/Address.tsx b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/substeps/Address.tsx new file mode 100644 index 000000000000..cd9533b4d66f --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/substeps/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 getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; + +type AddressProps = SubStepProps; + +const {COMPANY_STREET, COMPANY_ZIP_CODE, COMPANY_STATE, COMPANY_CITY, COMPANY_COUNTRY} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; + +const INPUT_KEYS = { + street: COMPANY_STREET, + city: COMPANY_CITY, + state: COMPANY_STATE, + zipCode: COMPANY_ZIP_CODE, + country: COMPANY_COUNTRY, +}; +const STEP_FIELDS = [COMPANY_STREET, COMPANY_CITY, COMPANY_STATE, COMPANY_ZIP_CODE, COMPANY_COUNTRY]; +const STEP_FIELDS_WITHOUT_STATE = [COMPANY_STREET, COMPANY_CITY, COMPANY_ZIP_CODE, COMPANY_COUNTRY]; + +function Address({onNext, onMove, isEditing}: AddressProps) { + const {translate} = useLocalize(); + + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + + const onyxValues = useMemo(() => getSubstepValues(INPUT_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); + + const businessStepCountryDraftValue = onyxValues[COMPANY_COUNTRY]; + const countryStepCountryDraftValue = reimbursementAccountDraft?.[INPUT_IDS.ADDITIONAL_DATA.COUNTRY] ?? ''; + const countryInitialValue = + businessStepCountryDraftValue !== '' && businessStepCountryDraftValue !== countryStepCountryDraftValue ? businessStepCountryDraftValue : countryStepCountryDraftValue; + + const defaultValues = { + street: onyxValues[COMPANY_STREET] ?? '', + city: onyxValues[COMPANY_CITY] ?? '', + state: onyxValues[COMPANY_STATE] ?? '', + zipCode: onyxValues[COMPANY_ZIP_CODE] ?? '', + country: businessStepCountryDraftValue ?? countryInitialValue, + }; + + // 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 stepFields = shouldDisplayStateSelector ? STEP_FIELDS : STEP_FIELDS_WITHOUT_STATE; + + 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={translate('businessInfoStep.whatsTheBusinessAddress')} + formPOBoxDisclaimer={translate('common.noPO')} + onSubmit={handleSubmit} + stepFields={stepFields} + inputFieldsIDs={INPUT_KEYS} + defaultValues={defaultValues} + onCountryChange={handleCountryChange} + shouldDisplayStateSelector={shouldDisplayStateSelector} + shouldDisplayCountrySelector + /> + ); +} + +Address.displayName = 'Address'; + +export default Address; diff --git a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/substeps/BusinessType.tsx b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/substeps/BusinessType.tsx new file mode 100644 index 000000000000..bd083c2dd535 --- /dev/null +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/substeps/BusinessType.tsx @@ -0,0 +1,87 @@ +import React, {useCallback} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import PushRowWithModal from '@components/PushRowWithModal'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useReimbursementAccountStepFormSubmit from '@hooks/useReimbursementAccountStepFormSubmit'; +import type {SubStepProps} from '@hooks/useSubStep/types'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import {applicantType, natureOfBusiness} from '@pages/ReimbursementAccount/NonUSD/BusinessInfo/mockedCorpayLists'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; + +type BusinessTypeProps = SubStepProps; + +const {BUSINESS_CATEGORY, APPLICANT_TYPE_ID} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; +const STEP_FIELDS = [BUSINESS_CATEGORY, APPLICANT_TYPE_ID]; + +const INCORPORATION_TYPE_LIST_OPTIONS = applicantType.reduce((accumulator, currentValue) => { + accumulator[currentValue.name] = currentValue.stringValue; + return accumulator; +}, {} as Record); +const BUSINESS_CATEGORY_LIST_OPTIONS = natureOfBusiness.reduce((accumulator, currentValue) => { + accumulator[currentValue.name] = currentValue.stringValue; + return accumulator; +}, {} as Record); + +function BusinessType({onNext, isEditing}: BusinessTypeProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + + const incorporationTypeDefaultValue = reimbursementAccount?.achData?.additionalData?.corpay?.[APPLICANT_TYPE_ID] ?? reimbursementAccountDraft?.[APPLICANT_TYPE_ID] ?? ''; + const businessCategoryDefaultValue = reimbursementAccount?.achData?.additionalData?.corpay?.[BUSINESS_CATEGORY] ?? reimbursementAccountDraft?.[BUSINESS_CATEGORY] ?? ''; + + const validate = useCallback((values: FormOnyxValues): FormInputErrors => { + return ValidationUtils.getFieldRequiredErrors(values, STEP_FIELDS); + }, []); + + const handleSubmit = useReimbursementAccountStepFormSubmit({ + fieldIds: STEP_FIELDS, + onNext, + shouldSaveDraft: true, + }); + + return ( + + {translate('businessInfoStep.whatTypeOfBusinessIsIt')} + + + + ); +} + +BusinessType.displayName = 'BusinessType'; + +export default BusinessType; diff --git a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/substeps/Confirmation.tsx b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/substeps/Confirmation.tsx index 9ff2b0e57de9..d0f26feccf0f 100644 --- a/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/substeps/Confirmation.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/BusinessInfo/substeps/Confirmation.tsx @@ -1,16 +1,58 @@ -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 {annualVolumeRange, applicantType, natureOfBusiness} from '@pages/ReimbursementAccount/NonUSD/BusinessInfo/mockedCorpayLists'; +import getSubstepValues from '@pages/ReimbursementAccount/utils/getSubstepValues'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/ReimbursementAccountForm'; -function Confirmation({onNext}: SubStepProps) { +const BUSINESS_INFO_STEP_KEYS = INPUT_IDS.ADDITIONAL_DATA.CORPAY; +const { + COMPANY_NAME, + BUSINESS_REGISTRATION_INCORPORATION_NUMBER, + COMPANY_COUNTRY, + COMPANY_STREET, + COMPANY_CITY, + COMPANY_STATE, + COMPANY_ZIP_CODE, + BUSINESS_CONTACT_NUMBER, + BUSINESS_CONFIRMATION_EMAIL, + FORMATION_INCORPORATION_COUNTRY_CODE, + ANNUAL_VOLUME, + APPLICANT_TYPE_ID, + BUSINESS_CATEGORY, +} = INPUT_IDS.ADDITIONAL_DATA.CORPAY; + +const displayStringValue = (list: Array<{id: string; name: string; stringValue: string}>, matchingName: string) => { + return list.find((item) => item.name === matchingName)?.stringValue ?? ''; +}; + +const displayAddress = (street: string, city: string, state: string, zipCode: string, country: string): string => { + return country === CONST.COUNTRY.US || country === CONST.COUNTRY.CA ? `${street}, ${city}, ${state}, ${zipCode}, ${country}` : `${street}, ${city}, ${zipCode}, ${country}`; +}; + +function Confirmation({onNext, onMove}: SubStepProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); + const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); + const [reimbursementAccountDraft] = useOnyx(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT); + + const values = useMemo(() => getSubstepValues(BUSINESS_INFO_STEP_KEYS, reimbursementAccountDraft, reimbursementAccount), [reimbursementAccount, reimbursementAccountDraft]); + + const paymentVolume = useMemo(() => displayStringValue(annualVolumeRange, values[ANNUAL_VOLUME]), [values]); + const businessCategory = useMemo(() => displayStringValue(natureOfBusiness, values[BUSINESS_CATEGORY]), [values]); + const businessType = useMemo(() => displayStringValue(applicantType, values[APPLICANT_TYPE_ID]), [values]); + return ( {({safeAreaPaddingBottomStyle}) => ( @@ -18,6 +60,79 @@ function Confirmation({onNext}: SubStepProps) { style={styles.pt0} contentContainerStyle={[styles.flexGrow1, safeAreaPaddingBottomStyle]} > + {translate('businessInfoStep.letsDoubleCheck')} + { + onMove(0); + }} + /> + { + onMove(3); + }} + /> + { + onMove(1); + }} + /> + { + onMove(2); + }} + /> + { + onMove(2); + }} + /> + { + onMove(5); + }} + /> + { + onMove(4); + }} + /> + { + onMove(5); + }} + /> + { + onMove(6); + }} + />