diff --git a/src/libs/CountryUtils.ts b/src/libs/CountryUtils.ts new file mode 100644 index 000000000000..cc4d6a2c3279 --- /dev/null +++ b/src/libs/CountryUtils.ts @@ -0,0 +1,39 @@ +import CONST from '@src/CONST'; +import type {Country} from '@src/CONST'; +import type {PersonalDetailsForm} from '@src/types/form'; +import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; + +type AddressType = PersonalDetailsForm | Address | undefined; + +/** + * Normalizes the address containing a country field by converting country names to country codes. + * Handles the case where old data has "United States" instead of "US". + */ +function normalizeCountryCode(data: AddressType): AddressType { + if (!data?.country) { + return data; + } + + const normalizedCountry = getCountryCode(data.country); + + if (!normalizedCountry) { + return data; + } + + return { + ...data, + country: normalizedCountry, + }; +} + +function getCountryCode(countryValue: string | undefined): Country | undefined { + for (const [code, name] of Object.entries(CONST.ALL_COUNTRIES)) { + if (name === countryValue) { + return code as Country; + } + } + + return countryValue as Country | undefined; +} + +export {normalizeCountryCode, getCountryCode}; diff --git a/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx b/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx index 161e357f45a9..dec00be5e973 100644 --- a/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx +++ b/src/pages/MissingPersonalDetails/MissingPersonalDetailsContent.tsx @@ -12,6 +12,7 @@ import useSubStep from '@hooks/useSubStep'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearDraftValues} from '@libs/actions/FormActions'; import {updatePersonalDetailsAndShipExpensifyCards} from '@libs/actions/PersonalDetails'; +import {normalizeCountryCode} from '@libs/CountryUtils'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -41,7 +42,7 @@ function MissingPersonalDetailsContent({privatePersonalDetails, draftValues}: Mi const ref: ForwardedRef = useRef(null); - const values = useMemo(() => getSubstepValues(privatePersonalDetails, draftValues), [privatePersonalDetails, draftValues]); + const values = useMemo(() => normalizeCountryCode(getSubstepValues(privatePersonalDetails, draftValues)) as PersonalDetailsForm, [privatePersonalDetails, draftValues]); const startFrom = useMemo(() => getInitialSubstep(values), [values]); diff --git a/src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx b/src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx index f436add45d62..6f1c31804245 100644 --- a/src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx @@ -2,6 +2,7 @@ import React, {useMemo} from 'react'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import {normalizeCountryCode} from '@libs/CountryUtils'; import {getCurrentAddress} from '@libs/PersonalDetailsUtils'; import AddressPage from '@pages/AddressPage'; import type {FormOnyxValues} from '@src/components/Form/types'; @@ -33,7 +34,7 @@ function PersonalAddressPage() { const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); const [defaultCountry, defaultCountryStatus] = useOnyx(ONYXKEYS.COUNTRY, {canBeMissing: true}); const isLoading = isLoadingOnyxValue(defaultCountryStatus); - const address = useMemo(() => getCurrentAddress(privatePersonalDetails), [privatePersonalDetails]); + const address = useMemo(() => normalizeCountryCode(getCurrentAddress(privatePersonalDetails)) as Address, [privatePersonalDetails]); if (isLoading) { return ; } diff --git a/tests/unit/CountryUtils.test.ts b/tests/unit/CountryUtils.test.ts new file mode 100644 index 000000000000..2d01ca76d1f9 --- /dev/null +++ b/tests/unit/CountryUtils.test.ts @@ -0,0 +1,153 @@ +import {getCountryCode, normalizeCountryCode} from '@libs/CountryUtils'; +import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; + +describe('CountryUtils', () => { + describe('getCountryCode', () => { + it('should return the same value if it is already a valid country code', () => { + expect(getCountryCode('US')).toBe('US'); + expect(getCountryCode('CA')).toBe('CA'); + expect(getCountryCode('GB')).toBe('GB'); + expect(getCountryCode('EG')).toBe('EG'); + }); + + it('should return the country code when given a country name', () => { + expect(getCountryCode('United States')).toBe('US'); + expect(getCountryCode('Canada')).toBe('CA'); + expect(getCountryCode('United Kingdom')).toBe('GB'); + expect(getCountryCode('Egypt')).toBe('EG'); + }); + + it('should return original value for invalid country names or codes', () => { + expect(getCountryCode('Invalid Country')).toBe('Invalid Country'); + expect(getCountryCode('XX')).toBe('XX'); + expect(getCountryCode('123')).toBe('123'); + expect(getCountryCode('MISSING TRANSLATION')).toBe('MISSING TRANSLATION'); + }); + + it('should handle edge cases with special characters', () => { + expect(getCountryCode('Bosnia & Herzegovina')).toBe('BA'); + }); + + it('should be case sensitive for country names', () => { + expect(getCountryCode('united states')).toBe('united states'); + expect(getCountryCode('UNITED STATES')).toBe('UNITED STATES'); + expect(getCountryCode('United States')).toBe('US'); + }); + + it('should convert common country names to codes', () => { + expect(getCountryCode('United States')).toBe('US'); + }); + + it('should handle multiple country formats correctly', () => { + const testCases = [ + {name: 'Afghanistan', code: 'AF'}, + {name: 'Australia', code: 'AU'}, + {name: 'Brazil', code: 'BR'}, + {name: 'China', code: 'CN'}, + {name: 'France', code: 'FR'}, + {name: 'Germany', code: 'DE'}, + {name: 'India', code: 'IN'}, + {name: 'Japan', code: 'JP'}, + {name: 'Mexico', code: 'MX'}, + {name: 'Russia', code: 'RU'}, + ]; + + testCases.forEach(({name, code}) => { + expect(getCountryCode(name)).toBe(code); + expect(getCountryCode(code)).toBe(code); + }); + }); + }); + + describe('normalizeCountryCode', () => { + it('should return undefined when data is undefined', () => { + expect(normalizeCountryCode(undefined)).toBeUndefined(); + }); + + it('should return data unchanged when country field is missing', () => { + const data = {street: '123 Main St', city: 'New York', state: 'NY'}; + expect(normalizeCountryCode(data)).toEqual(data); + }); + + it('should return data unchanged when country is undefined', () => { + const data = {street: '123 Main St', city: 'New York', state: 'NY', country: undefined}; + expect(normalizeCountryCode(data)).toEqual(data); + }); + + it('should convert country name to country code', () => { + const data = {street: '123 Main St', city: 'New York', state: 'NY', country: 'United States'} as unknown as Address; + const result = normalizeCountryCode(data); + expect(result).toEqual({street: '123 Main St', city: 'New York', state: 'NY', country: 'US'}); + }); + + it('should preserve country code if already a valid code', () => { + const data: Address = {street: '456 Oak Ave', city: 'Toronto', state: 'ON', country: 'CA'}; + const result = normalizeCountryCode(data); + expect(result).toEqual({street: '456 Oak Ave', city: 'Toronto', state: 'ON', country: 'CA'}); + }); + + it('should handle multiple country name conversions', () => { + const testCases = [ + {input: 'United States', expected: 'US'}, + {input: 'Canada', expected: 'CA'}, + {input: 'United Kingdom', expected: 'GB'}, + {input: 'Germany', expected: 'DE'}, + {input: 'France', expected: 'FR'}, + {input: 'Japan', expected: 'JP'}, + {input: 'Australia', expected: 'AU'}, + ]; + + testCases.forEach(({input, expected}) => { + const data = {street: '789 Test St', city: 'Test City', country: input} as unknown as Address; + const result = normalizeCountryCode(data); + expect(result?.country).toBe(expected); + }); + }); + + it('should preserve invalid country values', () => { + const data = {street: '789 Test St', city: 'Test City', country: 'Invalid Country'} as unknown as Address; + const result = normalizeCountryCode(data); + expect(result).toEqual({street: '789 Test St', city: 'Test City', country: 'Invalid Country'}); + }); + + it('should handle special characters in country names', () => { + const data = {street: '123 Main St', city: 'Sarajevo', country: 'Bosnia & Herzegovina'} as unknown as Address; + const result = normalizeCountryCode(data); + expect(result).toEqual({street: '123 Main St', city: 'Sarajevo', country: 'BA'}); + }); + + it('should be case sensitive when normalizing country names', () => { + const dataLowerCase = {street: '789 Test St', city: 'Test City', country: 'united states'} as unknown as Address; + const dataUpperCase = {street: '789 Test St', city: 'Test City', country: 'UNITED STATES'} as unknown as Address; + const dataProperCase = {street: '789 Test St', city: 'Test City', country: 'United States'} as unknown as Address; + + expect(normalizeCountryCode(dataLowerCase)).toEqual({street: '789 Test St', city: 'Test City', country: 'united states'}); + expect(normalizeCountryCode(dataUpperCase)).toEqual({street: '789 Test St', city: 'Test City', country: 'UNITED STATES'}); + expect(normalizeCountryCode(dataProperCase)).toEqual({street: '789 Test St', city: 'Test City', country: 'US'}); + }); + + it('should preserve all other fields in the data object', () => { + const data = { + street: '123 Main St', + city: 'Los Angeles', + state: 'CA', + zipCode: '90001', + country: 'United States', + } as unknown as Address; + const result = normalizeCountryCode(data); + expect(result).toEqual({ + street: '123 Main St', + city: 'Los Angeles', + state: 'CA', + zipCode: '90001', + country: 'US', + }); + }); + + it('should handle MISSING TRANSLATION value', () => { + const data = {street: '789 Test St', city: 'Test City', country: 'MISSING TRANSLATION'} as unknown as Address; + const result = normalizeCountryCode(data); + expect(result).toEqual({street: '789 Test St', city: 'Test City', country: 'MISSING TRANSLATION'}); + }); + }); +});