Skip to content

Commit

Permalink
[TS migration] Migrate 'AddressForm.js' component to TypeScript
Browse files Browse the repository at this point in the history
  • Loading branch information
pasyukevich committed Jan 17, 2024
1 parent 5b82342 commit e4d33ce
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 57 deletions.
142 changes: 88 additions & 54 deletions src/components/AddressForm.js → src/components/AddressForm.tsx
Original file line number Diff line number Diff line change
@@ -1,109 +1,137 @@
import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import type {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
import React, {useCallback} from 'react';
import {View} from 'react-native';
import _ from 'underscore';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import type * as Localize from '@libs/Localize';
import Navigation from '@libs/Navigation/Navigation';
import * as ValidationUtils from '@libs/ValidationUtils';
import CONST from '@src/CONST';
import type {OnyxFormKey} from '@src/ONYXKEYS';
import type {AddressForm as AddressFormValues} from '@src/types/onyx';
import AddressSearch from './AddressSearch';
import CountrySelector from './CountrySelector';
import FormProvider from './Form/FormProvider';
import InputWrapper from './Form/InputWrapper';
import StatePicker from './StatePicker';
import TextInput from './TextInput';

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

/** Address country field */
country: PropTypes.string,
country?: keyof typeof CONST.COUNTRY_ZIP_REGEX_DATA | '';

/** Address state field */
state: PropTypes.string,
state?: keyof typeof COMMON_CONST.STATES | '';

/** 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?: (data: string, key: string) => void;

/** Callback which is executed when the user submits his address changes */
onSubmit: PropTypes.func.isRequired,
onSubmit: () => 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: OnyxFormKey;
};

const defaultProps = {
city: '',
country: '',
onAddressChanged: () => {},
shouldSaveDraft: false,
state: '',
street1: '',
street2: '',
submitButtonText: '',
zip: '',
type ValidatorErrors = {
addressLine1?: string;
city?: string;
country?: string;
state?: string;
zipPostCode?: Localize.MaybePhraseKey;
};

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'], '');

let zipSampleFormat = '';

if (country) {
const countryData = CONST.COUNTRY_ZIP_REGEX_DATA[country];
if (countryData && 'samples' in countryData) {
zipSampleFormat = countryData.samples;
}
}

const zipFormat = translate('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: AddressFormValues): ValidatorErrors => {
const errors: ValidatorErrors = {};
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] : {};

// 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');
let countrySpecificZipRegex;
let countryZipFormat;

if ('regex' in countryRegexDetails) {
countrySpecificZipRegex = countryRegexDetails.regex as RegExp;
}

if ('samples' in countryRegexDetails) {
countryZipFormat = countryRegexDetails.samples as string;
}

if (countrySpecificZipRegex) {
if (!countrySpecificZipRegex.test(values.zipPostCode.trim().toUpperCase())) {
if (ValidationUtils.isRequiredFulfilled(values.zipPostCode.trim())) {
errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', {zipFormat: countryZipFormat}];
errors.zipPostCode = ['privatePersonalDetails.error.incorrectZipFormat', {zipFormat: countryZipFormat ?? ''}];
} else {
errors.zipPostCode = 'common.error.fieldRequired';
}
Expand All @@ -116,6 +144,7 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
}, []);

return (
// @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript.
<FormProvider
style={[styles.flexGrow1, styles.mh5]}
formID={formID}
Expand All @@ -126,15 +155,16 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
>
<View>
<InputWrapper
// @ts-expect-error TODO: Remove this once InputWrapper (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript.
InputComponent={AddressSearch}
inputID="addressLine1"
label={translate('common.addressLine', {lineNumber: 1})}
onValueChange={(data, key) => {
onValueChange={(data: string, key: string) => {
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: 'addressLine1',
street2: 'addressLine2',
Expand All @@ -149,19 +179,21 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
</View>
<View style={styles.formSpaceVertical} />
<InputWrapper
// @ts-expect-error TODO: Remove this once InputWrapper (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript.
InputComponent={TextInput}
inputID="addressLine2"
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}
/>
<View style={styles.formSpaceVertical} />
<View style={styles.mhn5}>
<InputWrapper
// @ts-expect-error TODO: Remove this once InputWrapper (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript.
InputComponent={CountrySelector}
inputID="country"
value={country}
Expand All @@ -172,6 +204,7 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
{isUSAForm ? (
<View style={styles.mhn5}>
<InputWrapper
// @ts-expect-error TODO: Remove this once InputWrapper (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript.
InputComponent={StatePicker}
inputID="state"
defaultValue={state}
Expand All @@ -181,12 +214,13 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
</View>
) : (
<InputWrapper
// @ts-expect-error TODO: Remove this once InputWrapper (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript.
InputComponent={TextInput}
inputID="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 @@ -195,26 +229,28 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
)}
<View style={styles.formSpaceVertical} />
<InputWrapper
// @ts-expect-error TODO: Remove this once InputWrapper (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript.
InputComponent={TextInput}
inputID="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}
shouldSaveDraft={shouldSaveDraft}
/>
<View style={styles.formSpaceVertical} />
<InputWrapper
// @ts-expect-error TODO: Remove this once InputWrapper (https://github.com/Expensify/App/issues/25109) is migrated to TypeScript.
InputComponent={TextInput}
inputID="zipPostCode"
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 @@ -224,8 +260,6 @@ function AddressForm({city, country, formID, onAddressChanged, onSubmit, shouldS
);
}

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

export default AddressForm;
2 changes: 1 addition & 1 deletion src/libs/Navigation/Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ function goBack(fallbackRoute?: Route, shouldEnforceFallback = false, shouldPopT
/**
* 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
33 changes: 32 additions & 1 deletion src/types/onyx/Form.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type {CONST as COMMON_CONST} from 'expensify-common/lib/CONST';
import type CONST from '@src/CONST';
import type * as OnyxCommon from './OnyxCommon';

type Form = {
Expand All @@ -21,6 +23,35 @@ type DateOfBirthForm = Form & {
dob?: string;
};

type AddressForm = Form & {
/** Address line 1 for delivery */
addressLine1: string;

/** Address line 2 for delivery */
addressLine2: string;

/** City for delivery */
city: string;

/** Country for delivery */
country: keyof typeof CONST.COUNTRY_ZIP_REGEX_DATA | '';

/** First name for delivery */
legalFirstName: string;

/** Last name for delivery */
legalLastName: string;

/** Phone number for delivery */
phoneNumber: string;

/** State for delivery */
state: keyof typeof COMMON_CONST.STATES | '';

/** Zip code for delivery */
zipPostCode: string;
};

export default Form;

export type {AddDebitCardForm, DateOfBirthForm};
export type {AddDebitCardForm, DateOfBirthForm, AddressForm};
3 changes: 2 additions & 1 deletion src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type Credentials from './Credentials';
import type Currency from './Currency';
import type CustomStatusDraft from './CustomStatusDraft';
import type Download from './Download';
import type {AddDebitCardForm, DateOfBirthForm} from './Form';
import type {AddDebitCardForm, AddressForm, DateOfBirthForm} from './Form';
import type Form from './Form';
import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji';
import type {FundList} from './Fund';
Expand Down Expand Up @@ -70,6 +70,7 @@ export type {
Account,
AccountData,
AddDebitCardForm,
AddressForm,
BankAccount,
BankAccountList,
Beta,
Expand Down

0 comments on commit e4d33ce

Please sign in to comment.