Skip to content

Commit

Permalink
Merge pull request #34149 from pasyukevich/feature/migrate-AddressForm
Browse files Browse the repository at this point in the history
[TS migration] Migrate 'AddressForm.js' component to TypeScript
  • Loading branch information
jasperhuangg authored Mar 4, 2024
2 parents afb4a51 + b45851b commit 34ce9e0
Show file tree
Hide file tree
Showing 11 changed files with 108 additions and 80 deletions.
131 changes: 73 additions & 58 deletions src/components/AddressForm.js → src/components/AddressForm.tsx
Original file line number Diff line number Diff line change
@@ -1,115 +1,132 @@
import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
import type {MaybePhraseKey} from '@libs/Localize';
import Navigation from '@libs/Navigation/Navigation';
import * as ValidationUtils from '@libs/ValidationUtils';
import CONST from '@src/CONST';
import type {Country} from '@src/CONST';
import type ONYXKEYS from '@src/ONYXKEYS';
import INPUT_IDS from '@src/types/form/HomeAddressForm';
import type {Errors} from '@src/types/onyx/OnyxCommon';
import AddressSearch from './AddressSearch';
import CountrySelector from './CountrySelector';
import FormProvider from './Form/FormProvider';
import InputWrapper from './Form/InputWrapper';
import type {FormOnyxValues} from './Form/types';
import StatePicker from './StatePicker';
import TextInput from './TextInput';

const propTypes = {
type CountryZipRegex = {
regex?: RegExp;
samples?: string;
};

type AddressFormProps = {
/** Address city field */
city: PropTypes.string,
city?: string;

/** Address country field */
country: PropTypes.string,
country?: Country | '';

/** Address state field */
state: PropTypes.string,
state?: string;

/** Address street line 1 field */
street1: PropTypes.string,
street1?: string;

/** Address street line 2 field */
street2: PropTypes.string,
street2?: string;

/** Address zip code field */
zip: PropTypes.string,
zip?: string;

/** Callback which is executed when the user changes address, city or state */
onAddressChanged: PropTypes.func,
onAddressChanged?: (value: unknown, key: unknown) => void;

/** Callback which is executed when the user submits his address changes */
onSubmit: PropTypes.func.isRequired,
onSubmit: (values: FormOnyxValues<typeof ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM | typeof ONYXKEYS.FORMS.HOME_ADDRESS_FORM>) => void;

/** Whether or not should the form data should be saved as draft */
shouldSaveDraft: PropTypes.bool,
shouldSaveDraft?: boolean;

/** Text displayed on the bottom submit button */
submitButtonText: PropTypes.string,
submitButtonText?: string;

/** A unique Onyx key identifying the form */
formID: PropTypes.string.isRequired,
formID: typeof ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM | typeof ONYXKEYS.FORMS.HOME_ADDRESS_FORM;
};

const defaultProps = {
city: '',
country: '',
onAddressChanged: () => {},
shouldSaveDraft: false,
state: '',
street1: '',
street2: '',
submitButtonText: '',
zip: '',
};

function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldSaveDraft, state, street1, street2, submitButtonText, zip}) {
function AddressForm({
city = '',
country = '',
formID,
onAddressChanged = () => {},
onSubmit,
shouldSaveDraft = false,
state = '',
street1 = '',
street2 = '',
submitButtonText = '',
zip = '',
}: AddressFormProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const zipSampleFormat = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, [country, 'samples'], '');
const zipFormat = ['common.zipCodeExampleFormat', {zipSampleFormat}];

const zipSampleFormat = (country && (CONST.COUNTRY_ZIP_REGEX_DATA[country] as CountryZipRegex)?.samples) ?? '';

const zipFormat: MaybePhraseKey = ['common.zipCodeExampleFormat', {zipSampleFormat}];

const isUSAForm = country === CONST.COUNTRY.US;

/**
* @param {Function} translate - translate function
* @param {Boolean} isUSAForm - selected country ISO code is US
* @param {Object} values - form input values
* @returns {Object} - An object containing the errors for each inputID
* @param translate - translate function
* @param isUSAForm - selected country ISO code is US
* @param values - form input values
* @returns - An object containing the errors for each inputID
*/
const validator = useCallback((values) => {
const errors = {};
const requiredFields = ['addressLine1', 'city', 'country', 'state'];

const validator = useCallback((values: FormOnyxValues<typeof ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM | typeof ONYXKEYS.FORMS.HOME_ADDRESS_FORM>): Errors => {
const errors: Errors & {
zipPostCode?: string | string[];
} = {};
const requiredFields = ['addressLine1', 'city', 'country', 'state'] as const;

// Check "State" dropdown is a valid state if selected Country is USA
if (values.country === CONST.COUNTRY.US && !COMMON_CONST.STATES[values.state]) {
if (values.country === CONST.COUNTRY.US && !values.state) {
errors.state = 'common.error.fieldRequired';
}

// Add "Field required" errors if any required field is empty
_.each(requiredFields, (fieldKey) => {
if (ValidationUtils.isRequiredFulfilled(values[fieldKey])) {
requiredFields.forEach((fieldKey) => {
const fieldValue = values[fieldKey] ?? '';
if (ValidationUtils.isRequiredFulfilled(fieldValue)) {
return;
}

errors[fieldKey] = 'common.error.fieldRequired';
});

// If no country is selected, default value is an empty string and there's no related regex data so we default to an empty object
const countryRegexDetails = lodashGet(CONST.COUNTRY_ZIP_REGEX_DATA, values.country, {});
const countryRegexDetails = (values.country ? CONST.COUNTRY_ZIP_REGEX_DATA?.[values.country] : {}) as CountryZipRegex;

// The postal code system might not exist for a country, so no regex either for them.
const countrySpecificZipRegex = lodashGet(countryRegexDetails, 'regex');
const countryZipFormat = lodashGet(countryRegexDetails, 'samples');
const countrySpecificZipRegex = countryRegexDetails?.regex;
const countryZipFormat = countryRegexDetails?.samples ?? '';

ErrorUtils.addErrorMessage(errors, 'firstName', 'bankAccount.error.firstName');

if (countrySpecificZipRegex) {
if (countrySpecificZipRegex && values.zipPostCode) {
if (!countrySpecificZipRegex.test(values.zipPostCode.trim().toUpperCase())) {
if (ValidationUtils.isRequiredFulfilled(values.zipPostCode.trim())) {
errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', countryZipFormat];
} else {
errors.zipPostCode = 'common.error.fieldRequired';
}
}
} else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values.zipPostCode.trim().toUpperCase())) {
} else if (!CONST.GENERIC_ZIP_CODE_REGEX.test(values?.zipPostCode?.trim()?.toUpperCase() ?? '')) {
errors.zipPostCode = 'privatePersonalDetails.error.incorrectZipFormat';
}

Expand All @@ -130,12 +147,12 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
InputComponent={AddressSearch}
inputID={INPUT_IDS.ADDRESS_LINE_1}
label={translate('common.addressLine', {lineNumber: 1})}
onValueChange={(data, key) => {
onValueChange={(data: unknown, key: unknown) => {
onAddressChanged(data, key);
// This enforces the country selector to use the country from address instead of the country from URL
Navigation.setParams({country: undefined});
}}
defaultValue={street1 || ''}
defaultValue={street1}
renamedInputKeys={{
street: INPUT_IDS.ADDRESS_LINE_1,
street2: INPUT_IDS.ADDRESS_LINE_2,
Expand All @@ -154,8 +171,8 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
inputID={INPUT_IDS.ADDRESS_LINE_2}
label={translate('common.addressLine', {lineNumber: 2})}
aria-label={translate('common.addressLine', {lineNumber: 2})}
role={CONST.ACCESSIBILITY_ROLE.TEXT}
defaultValue={street2 || ''}
role={CONST.ROLE.PRESENTATION}
defaultValue={street2}
maxLength={CONST.FORM_CHARACTER_LIMIT}
spellCheck={false}
shouldSaveDraft={shouldSaveDraft}
Expand Down Expand Up @@ -186,8 +203,8 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
inputID={INPUT_IDS.STATE}
label={translate('common.stateOrProvince')}
aria-label={translate('common.stateOrProvince')}
role={CONST.ACCESSIBILITY_ROLE.TEXT}
value={state || ''}
role={CONST.ROLE.PRESENTATION}
value={state}
maxLength={CONST.FORM_CHARACTER_LIMIT}
spellCheck={false}
onValueChange={onAddressChanged}
Expand All @@ -200,8 +217,8 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
inputID={INPUT_IDS.CITY}
label={translate('common.city')}
aria-label={translate('common.city')}
role={CONST.ACCESSIBILITY_ROLE.TEXT}
defaultValue={city || ''}
role={CONST.ROLE.PRESENTATION}
defaultValue={city}
maxLength={CONST.FORM_CHARACTER_LIMIT}
spellCheck={false}
onValueChange={onAddressChanged}
Expand All @@ -213,9 +230,9 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
inputID={INPUT_IDS.ZIP_POST_CODE}
label={translate('common.zipPostCode')}
aria-label={translate('common.zipPostCode')}
role={CONST.ACCESSIBILITY_ROLE.TEXT}
role={CONST.ROLE.PRESENTATION}
autoCapitalize="characters"
defaultValue={zip || ''}
defaultValue={zip}
maxLength={CONST.BANK_ACCOUNT.MAX_LENGTH.ZIP_CODE}
hint={zipFormat}
onValueChange={onAddressChanged}
Expand All @@ -225,8 +242,6 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
);
}

AddressForm.defaultProps = defaultProps;
AddressForm.displayName = 'AddressForm';
AddressForm.propTypes = propTypes;

export default AddressForm;
4 changes: 2 additions & 2 deletions src/components/AddressSearch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ type RenamedInputKeysProps = {
street2: string;
city: string;
state: string;
lat: string;
lng: string;
lat?: string;
lng?: string;
zipCode: string;
address?: string;
country?: string;
Expand Down
6 changes: 3 additions & 3 deletions src/components/CountrySelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ type CountrySelectorProps = {
errorText?: MaybePhraseKey;

/** Callback called when the country changes. */
onInputChange: (value?: string) => void;
onInputChange?: (value?: string) => void;

/** Current selected country */
value?: Country;
value?: Country | '';

/** inputID used by the Form component */
// eslint-disable-next-line react/no-unused-prop-types
Expand All @@ -29,7 +29,7 @@ type CountrySelectorProps = {
onBlur?: () => void;
};

function CountrySelector({errorText = '', value: countryCode, onInputChange, onBlur}: CountrySelectorProps, ref: ForwardedRef<View>) {
function CountrySelector({errorText = '', value: countryCode, onInputChange = () => {}, onBlur}: CountrySelectorProps, ref: ForwardedRef<View>) {
const styles = useThemeStyles();
const {translate} = useLocalize();

Expand Down
4 changes: 3 additions & 1 deletion src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type TextInput from '@components/TextInput';
import type ValuePicker from '@components/ValuePicker';
import type {MaybePhraseKey} from '@libs/Localize';
import type BusinessTypePicker from '@pages/ReimbursementAccount/BusinessInfo/substeps/TypeBusiness/BusinessTypePicker';
import type {Country} from '@src/CONST';
import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS';
import type {BaseForm} from '@src/types/form/Form';

Expand All @@ -42,11 +43,12 @@ type ValidInputs =
| typeof DatePicker
| typeof RadioButtons;

type ValueTypeKey = 'string' | 'boolean' | 'date';
type ValueTypeKey = 'string' | 'boolean' | 'date' | 'country';
type ValueTypeMap = {
string: string;
boolean: boolean;
date: Date;
country: Country | '';
};
type FormValue = ValueOf<ValueTypeMap>;

Expand Down
2 changes: 1 addition & 1 deletion src/libs/Navigation/Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ function closeFullScreen() {
/**
* Update route params for the specified route.
*/
function setParams(params: Record<string, unknown>, routeKey: string) {
function setParams(params: Record<string, unknown>, routeKey = '') {
navigationRef.current?.dispatch({
...CommonActions.setParams(params),
source: routeKey,
Expand Down
3 changes: 2 additions & 1 deletion src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
} from '@react-navigation/native';
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
import type {Country} from '@src/CONST';
import type NAVIGATORS from '@src/NAVIGATORS';
import type {HybridAppRoute, Route as Routes} from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
Expand Down Expand Up @@ -107,7 +108,7 @@ type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.PROFILE.LEGAL_NAME]: undefined;
[SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH]: undefined;
[SCREENS.SETTINGS.PROFILE.ADDRESS]: {
country?: string;
country?: Country | '';
};
[SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: {
backTo?: Routes;
Expand Down
3 changes: 2 additions & 1 deletion src/libs/actions/PersonalDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import DateUtils from '@libs/DateUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as UserUtils from '@libs/UserUtils';
import type {Country} from '@src/CONST';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
Expand Down Expand Up @@ -137,7 +138,7 @@ function updateDateOfBirth({dob}: DateOfBirthForm) {
Navigation.goBack();
}

function updateAddress(street: string, street2: string, city: string, state: string, zip: string, country: string) {
function updateAddress(street: string, street2: string, city: string, state: string, zip: string, country: Country | '') {
const parameters: UpdateHomeAddressParams = {
homeAddressStreet: street,
addressStreet2: street2,
Expand Down
Loading

0 comments on commit 34ce9e0

Please sign in to comment.