Skip to content

Commit 557fa54

Browse files
committed
Merge branch 'main' into fix/94561-part-1
2 parents 8eea604 + 0d736c7 commit 557fa54

4 files changed

Lines changed: 64 additions & 21 deletions

File tree

config/eslint/eslint.seatbelt.tsv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,7 @@
413413
"../../src/components/StatePicker/StateSelectorModal.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
414414
"../../src/components/StatePicker/index.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
415415
"../../src/components/StatusBadge.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
416-
"../../src/components/SubStepForms/AddressStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 5
416+
"../../src/components/SubStepForms/AddressStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2
417417
"../../src/components/SubStepForms/AgreementsFullStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 8
418418
"../../src/components/SubStepForms/CountryFullStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
419419
"../../src/components/SubStepForms/DateOfBirthStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2

src/components/SubStepForms/AddressStep.tsx

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import React, {useCallback, useEffect, useRef} from 'react';
22
import {View} from 'react-native';
33
import FormProvider from '@components/Form/FormProvider';
4-
import type {FormInputErrors, FormOnyxKeys, FormOnyxValues, FormRef, FormValue} from '@components/Form/types';
4+
import type {FormInputErrors, FormOnyxKeys, FormOnyxValues, FormRef} from '@components/Form/types';
55
import PatriotActLink from '@components/PatriotActLink';
66
import Text from '@components/Text';
77
import useLocalize from '@hooks/useLocalize';
88
import type {SubStepProps} from '@hooks/useSubStep/types';
99
import useThemeStyles from '@hooks/useThemeStyles';
1010
import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
11-
import {getFieldRequiredErrors, getInvalidAddressErrorTranslationPath, isValidZipCode, isValidZipCodeInternational} from '@libs/ValidationUtils';
11+
import {getCountryZipRegexDetails, getFieldRequiredErrors, getInvalidAddressErrorTranslationPath, isValidZipCode, isValidZipCodeForCountry} from '@libs/ValidationUtils';
1212
import AddressFormFields from '@pages/ReimbursementAccount/AddressFormFields';
1313
import HelpLinks from '@pages/ReimbursementAccount/USD/Requestor/PersonalInfo/HelpLinks';
1414
import {setDraftValues} from '@userActions/FormActions';
@@ -32,6 +32,15 @@ type AddressInputIDs = {
3232
country?: string;
3333
};
3434

35+
function getStringFormValue<TFormID extends keyof OnyxFormValuesMapping>(values: FormOnyxValues<TFormID>, fieldID?: string): string {
36+
if (!fieldID) {
37+
return '';
38+
}
39+
40+
const value = values[fieldID as keyof FormOnyxValues<TFormID>];
41+
return typeof value === 'string' ? value : '';
42+
}
43+
3544
type AddressStepProps<TFormID extends keyof OnyxFormValuesMapping> = SubStepProps &
3645
ForwardedFSClassProps & {
3746
/** The ID of the form */
@@ -138,23 +147,29 @@ function AddressStep<TFormID extends keyof OnyxFormValuesMapping>({
138147
(values: FormOnyxValues<TFormID>): FormInputErrors<TFormID> => {
139148
const errors = getFieldRequiredErrors(values, stepFields, translate);
140149

141-
const street = values[inputFieldsIDs.street as keyof typeof values];
142-
const streetValue = street as FormValue;
143-
const streetError = getInvalidAddressErrorTranslationPath(streetValue);
150+
const street = getStringFormValue(values, inputFieldsIDs.street);
151+
const streetError = getInvalidAddressErrorTranslationPath(street);
144152
if (street && streetError) {
145153
// @ts-expect-error type mismatch to be fixed
146154
errors[inputFieldsIDs.street] = translate(streetError);
147155
}
148156

149-
const zipCode = values[inputFieldsIDs.zipCode as keyof typeof values];
150-
if (shouldValidateZipCodeFormat && zipCode && (shouldDisplayCountrySelector ? !isValidZipCodeInternational(zipCode as string) : !isValidZipCode(zipCode as string))) {
157+
const zipCode = getStringFormValue(values, inputFieldsIDs.zipCode);
158+
const selectedCountry = (inputFieldsIDs.country ? getStringFormValue(values, inputFieldsIDs.country) : defaultValues.country) as Country | '';
159+
const shouldValidateSelectedCountryZip = shouldDisplayCountrySelector && !!inputFieldsIDs.country;
160+
161+
if (zipCode && shouldValidateSelectedCountryZip && !isValidZipCodeForCountry(zipCode, selectedCountry)) {
162+
const zipCodeSamples = getCountryZipRegexDetails(selectedCountry)?.samples;
163+
// @ts-expect-error type mismatch to be fixed
164+
errors[inputFieldsIDs.zipCode] = translate('privatePersonalDetails.error.incorrectZipFormat', zipCodeSamples);
165+
} else if (zipCode && shouldValidateZipCodeFormat && !isValidZipCode(zipCode)) {
151166
// @ts-expect-error type mismatch to be fixed
152167
errors[inputFieldsIDs.zipCode] = translate('bankAccount.error.zipCode');
153168
}
154169

155170
return errors;
156171
},
157-
[inputFieldsIDs.street, inputFieldsIDs.zipCode, shouldDisplayCountrySelector, shouldValidateZipCodeFormat, stepFields, translate],
172+
[defaultValues.country, inputFieldsIDs.country, inputFieldsIDs.street, inputFieldsIDs.zipCode, shouldDisplayCountrySelector, shouldValidateZipCodeFormat, stepFields, translate],
158173
);
159174

160175
return (
@@ -182,7 +197,6 @@ function AddressStep<TFormID extends keyof OnyxFormValuesMapping>({
182197
stateSelectorSearchInputTitle={stateSelectorSearchInputTitle}
183198
onCountryChange={onCountryChange}
184199
shouldAllowCountryChange={shouldAllowCountryChange}
185-
shouldValidateZipCodeFormat={shouldValidateZipCodeFormat}
186200
forwardedFSClass={forwardedFSClass}
187201
/>
188202
{!!shouldShowHelpLinks && (

src/libs/ValidationUtils.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {addYears, endOfMonth, format, isAfter, isBefore, isSameDay, isValid, isWithinInterval, parse, parseISO, startOfDay, subYears} from 'date-fns';
2-
import {PUBLIC_DOMAINS_SET, Str, TLD_REGEX, Url} from 'expensify-common';
2+
import {CONST as COMMON_CONST, PUBLIC_DOMAINS_SET, Str, TLD_REGEX, Url} from 'expensify-common';
33
import isEmpty from 'lodash/isEmpty';
44
import isObject from 'lodash/isObject';
55
import type {OnyxCollection} from 'react-native-onyx';
@@ -16,6 +16,11 @@ import {getPhoneNumberWithoutSpecialChars} from './LoginUtils';
1616
import {parsePhoneNumber} from './PhoneNumber';
1717
import StringUtils from './StringUtils';
1818

19+
type CountryZipRegex = {
20+
regex?: RegExp;
21+
samples?: string;
22+
};
23+
1924
/**
2025
* Implements the Luhn Algorithm, a checksum formula used to validate credit card
2126
* numbers.
@@ -205,6 +210,25 @@ function isValidZipCode(zipCode: string): boolean {
205210
return CONST.REGEX.ZIP_CODE.test(zipCode);
206211
}
207212

213+
function getCountryZipRegexDetails(country?: Country | ''): CountryZipRegex | undefined {
214+
if (!country) {
215+
return undefined;
216+
}
217+
218+
return COMMON_CONST.COUNTRY_ZIP_REGEX_DATA[country] as CountryZipRegex | undefined;
219+
}
220+
221+
function isValidZipCodeForCountry(zipCode: string, country?: Country | ''): boolean {
222+
const normalizedZipCode = zipCode.trim().toUpperCase();
223+
const countrySpecificZipRegex = getCountryZipRegexDetails(country)?.regex;
224+
225+
if (countrySpecificZipRegex) {
226+
return countrySpecificZipRegex.test(normalizedZipCode);
227+
}
228+
229+
return COMMON_CONST.GENERIC_ZIP_CODE_REGEX.test(normalizedZipCode);
230+
}
231+
208232
function isValidPaymentZipCode(zipCode: string): boolean {
209233
return CONST.REGEX.ALPHANUMERIC_WITH_SPACE_AND_HYPHEN.test(zipCode);
210234
}
@@ -863,6 +887,8 @@ export {
863887
isValidDebitCard,
864888
isValidIndustryCode,
865889
isValidZipCode,
890+
getCountryZipRegexDetails,
891+
isValidZipCodeForCountry,
866892
isValidPaymentZipCode,
867893
isRequiredFulfilled,
868894
getFieldRequiredErrors,

src/pages/ReimbursementAccount/AddressFormFields.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import TextInput from '@components/TextInput';
99
import useLocalize from '@hooks/useLocalize';
1010
import useThemeStyles from '@hooks/useThemeStyles';
1111
import type {ForwardedFSClassProps} from '@libs/Fullstory/types';
12+
import {getCountryZipRegexDetails} from '@libs/ValidationUtils';
1213
import CONST from '@src/CONST';
14+
import type {Country} from '@src/CONST';
1315
import type {TranslationPaths} from '@src/languages/types';
1416
import type {Address} from '@src/types/onyx/PrivatePersonalDetails';
1517

@@ -74,9 +76,6 @@ type AddressFormProps = ForwardedFSClassProps & {
7476

7577
/** Indicates if country can be changed by user */
7678
shouldAllowCountryChange?: boolean;
77-
78-
/** Indicates if zip code format should be validated */
79-
shouldValidateZipCodeFormat?: boolean;
8079
};
8180

8281
const PROVINCES_LIST_OPTIONS = (Object.keys(COMMON_CONST.PROVINCES) as Array<keyof typeof COMMON_CONST.PROVINCES>).reduce(
@@ -95,6 +94,10 @@ const STATES_LIST_OPTIONS = (Object.keys(COMMON_CONST.STATES) as Array<keyof typ
9594
{} as Record<string, string>,
9695
);
9796

97+
function isCountry(country: string): country is Country {
98+
return country in CONST.ALL_COUNTRIES;
99+
}
100+
98101
function AddressFormFields({
99102
shouldSaveDraft = false,
100103
defaultValues,
@@ -110,18 +113,18 @@ function AddressFormFields({
110113
stateSelectorSearchInputTitle,
111114
onCountryChange,
112115
shouldAllowCountryChange = true,
113-
shouldValidateZipCodeFormat = true,
114116
forwardedFSClass,
115117
}: AddressFormProps) {
116118
const {translate} = useLocalize();
117119
const styles = useThemeStyles();
118120

119-
const [countryInEditMode, setCountryInEditMode] = useState<string>(defaultValues?.country ?? CONST.COUNTRY.US);
120-
// When draft values are not being saved we need to relay on local state to determine the currently selected country
121-
const currentlySelectedCountry = shouldSaveDraft ? defaultValues?.country : countryInEditMode;
121+
const defaultCountry = defaultValues?.country === '' ? CONST.COUNTRY.US : (defaultValues?.country ?? CONST.COUNTRY.US);
122+
const [countryInEditMode, setCountryInEditMode] = useState<Country | ''>('');
123+
const currentlySelectedCountry = countryInEditMode || defaultCountry;
124+
const zipSampleFormat = getCountryZipRegexDetails(currentlySelectedCountry)?.samples ?? '';
122125

123126
const handleCountryChange = (country: unknown) => {
124-
if (typeof country === 'string' && country !== '') {
127+
if (typeof country === 'string' && isCountry(country)) {
125128
setCountryInEditMode(country);
126129
}
127130
onCountryChange?.(country);
@@ -186,11 +189,11 @@ function AddressFormFields({
186189
label={translate('common.zip')}
187190
accessibilityLabel={translate('common.zip')}
188191
role={CONST.ROLE.PRESENTATION}
189-
inputMode={shouldValidateZipCodeFormat ? CONST.INPUT_MODE.NUMERIC : undefined}
192+
inputMode={currentlySelectedCountry === CONST.COUNTRY.US ? CONST.INPUT_MODE.NUMERIC : undefined}
190193
value={values?.zipCode}
191194
defaultValue={defaultValues?.zipCode}
192195
errorText={errors?.zipCode ? translate('bankAccount.error.zipCode') : ''}
193-
hint={translate('common.zipCodeExampleFormat', COMMON_CONST.COUNTRY_ZIP_REGEX_DATA.US.samples)}
196+
hint={translate('common.zipCodeExampleFormat', zipSampleFormat)}
194197
containerStyles={styles.mt3}
195198
forwardedFSClass={forwardedFSClass}
196199
autoComplete="postal-code"

0 commit comments

Comments
 (0)