Skip to content

Commit

Permalink
Merge pull request #37426 from kubabutkiewicz/ts-migration/SettingsPr…
Browse files Browse the repository at this point in the history
…ofileDetails

[TS migration] Migrate 'SettingsProfileDetails' page to TypeScript
  • Loading branch information
tylerkaraszewski authored Feb 29, 2024
2 parents b5003f9 + daeb2a0 commit 96b66b0
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 352 deletions.
2 changes: 1 addition & 1 deletion src/components/Form/FormWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ function FormWrapper({
buttonText={submitButtonText}
isAlertVisible={((!isEmptyObject(errors) || !isEmptyObject(formState?.errorFields)) && !shouldHideFixErrorsAlert) || !!errorMessage}
isLoading={!!formState?.isLoading}
message={isEmptyObject(formState?.errorFields) ? errorMessage : undefined}
message={typeof errorMessage === 'string' && isEmptyObject(formState?.errorFields) ? errorMessage : undefined}
onSubmit={onSubmit}
footerContent={footerContent}
onFixTheErrorsLinkPressed={onFixTheErrorsLinkPressed}
Expand Down
9 changes: 7 additions & 2 deletions src/components/Form/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ import type AmountForm from '@components/AmountForm';
import type AmountTextInput from '@components/AmountTextInput';
import type CheckboxWithLabel from '@components/CheckboxWithLabel';
import type CountrySelector from '@components/CountrySelector';
import type DatePicker from '@components/DatePicker';
import type Picker from '@components/Picker';
import type RadioButtons from '@components/RadioButtons';
import type RoomNameInput from '@components/RoomNameInput';
import type SingleChoiceQuestion from '@components/SingleChoiceQuestion';
import type StatePicker from '@components/StatePicker';
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 {TranslationPaths} from '@src/languages/types';
import type {OnyxFormKey, OnyxValues} from '@src/ONYXKEYS';
import type {BaseForm} from '@src/types/form/Form';

Expand All @@ -38,6 +39,7 @@ type ValidInputs =
| typeof StatePicker
| typeof RoomNameInput
| typeof ValuePicker
| typeof DatePicker
| typeof RadioButtons;

type ValueTypeKey = 'string' | 'boolean' | 'date';
Expand Down Expand Up @@ -66,6 +68,9 @@ type InputComponentBaseProps<TValue extends ValueTypeKey = ValueTypeKey> = Input
isFocused?: boolean;
measureLayout?: (ref: unknown, callback: MeasureLayoutOnSuccessCallback) => void;
focus?: () => void;
label?: string;
minDate?: Date;
maxDate?: Date;
onTouched?: (event: GestureResponderEvent) => void;
onBlur?: (event: FocusEvent | NativeSyntheticEvent<TextInputFocusEventData>) => void;
onPressOut?: (event: GestureResponderEvent) => void;
Expand Down Expand Up @@ -123,6 +128,6 @@ type FormProps<TFormID extends OnyxFormKey = OnyxFormKey> = {

type InputRefs = Record<string, MutableRefObject<InputComponentBaseProps>>;

type FormInputErrors<TFormID extends OnyxFormKey = OnyxFormKey> = Partial<Record<FormOnyxKeys<TFormID>, TranslationPaths>>;
type FormInputErrors<TFormID extends OnyxFormKey = OnyxFormKey> = Partial<Record<FormOnyxKeys<TFormID>, MaybePhraseKey>>;

export type {FormProps, ValidInputs, InputComponentValueProps, FormValue, ValueTypeKey, FormOnyxValues, FormOnyxKeys, FormInputErrors, InputRefs, InputComponentBaseProps, ValueTypeMap};
9 changes: 7 additions & 2 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,13 @@ type SettingsNavigatorParamList = {
[SCREENS.SETTINGS.PROFILE.TIMEZONE_SELECT]: undefined;
[SCREENS.SETTINGS.PROFILE.LEGAL_NAME]: undefined;
[SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH]: undefined;
[SCREENS.SETTINGS.PROFILE.ADDRESS]: undefined;
[SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: undefined;
[SCREENS.SETTINGS.PROFILE.ADDRESS]: {
country?: string;
};
[SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: {
backTo?: Routes;
country: string;
};
[SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: {
backTo: Routes;
};
Expand Down
4 changes: 2 additions & 2 deletions src/libs/PersonalDetailsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ function getStreetLines(street = '') {
* @param privatePersonalDetails - details object
* @returns - formatted address
*/
function getFormattedAddress(privatePersonalDetails: PrivatePersonalDetails): string {
const {address} = privatePersonalDetails;
function getFormattedAddress(privatePersonalDetails: OnyxEntry<PrivatePersonalDetails>): string {
const {address} = privatePersonalDetails ?? {};
const [street1, street2] = getStreetLines(address?.street);
const formattedAddress =
formatPiece(street1) + formatPiece(street2) + formatPiece(address?.city) + formatPiece(address?.state) + formatPiece(address?.zip) + formatPiece(address?.country);
Expand Down
5 changes: 3 additions & 2 deletions src/libs/ValidationUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {OnyxFormKey} from '@src/ONYXKEYS';
import type {Report} from '@src/types/onyx';
import * as CardUtils from './CardUtils';
import DateUtils from './DateUtils';
import type {MaybePhraseKey} from './Localize';
import * as LoginUtils from './LoginUtils';
import {parsePhoneNumber} from './PhoneNumber';
import StringUtils from './StringUtils';
Expand Down Expand Up @@ -190,7 +191,7 @@ function meetsMaximumAgeRequirement(date: string): boolean {
/**
* Validate that given date is in a specified range of years before now.
*/
function getAgeRequirementError(date: string, minimumAge: number, maximumAge: number): string | Array<string | Record<string, string>> {
function getAgeRequirementError(date: string, minimumAge: number, maximumAge: number): MaybePhraseKey {
const currentDate = startOfDay(new Date());
const testDate = parse(date, CONST.DATE.FNS_FORMAT_STRING, currentDate);

Expand Down Expand Up @@ -360,7 +361,7 @@ function isValidPersonName(value: string) {
/**
* Checks if the provided string includes any of the provided reserved words
*/
function doesContainReservedWord(value: string, reservedWords: string[]): boolean {
function doesContainReservedWord(value: string, reservedWords: typeof CONST.DISPLAY_NAME.RESERVED_NAMES): boolean {
const valueToCheck = value.trim().toLowerCase();
return reservedWords.some((reservedWord) => valueToCheck.includes(reservedWord.toLowerCase()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import AddressForm from '@components/AddressForm';
import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
Expand All @@ -10,66 +10,47 @@ import useLocalize from '@hooks/useLocalize';
import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import * as PersonalDetails from '@userActions/PersonalDetails';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import type {PrivatePersonalDetails} from '@src/types/onyx';
import type {Address} from '@src/types/onyx/PrivatePersonalDetails';

const propTypes = {
/* Onyx Props */

type AddressPageOnyxProps = {
/** User's private personal details */
privatePersonalDetails: PropTypes.shape({
/** User's home address */
address: PropTypes.shape({
street: PropTypes.string,
city: PropTypes.string,
state: PropTypes.string,
zip: PropTypes.string,
country: PropTypes.string,
}),
}),

/** Route from navigation */
route: PropTypes.shape({
/** Params from the route */
params: PropTypes.shape({
/** Currently selected country */
country: PropTypes.string,
}),
}).isRequired,
privatePersonalDetails: OnyxEntry<PrivatePersonalDetails>;
};

const defaultProps = {
privatePersonalDetails: {
address: {
street: '',
city: '',
state: '',
zip: '',
country: '',
},
},
};
type AddressPageProps = StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.PROFILE.ADDRESS> & AddressPageOnyxProps;

/**
* Submit form to update user's first and last legal name
* @param {Object} values - form input values
* @param values - form input values
*/
function updateAddress(values) {
PersonalDetails.updateAddress(values.addressLine1.trim(), values.addressLine2.trim(), values.city.trim(), values.state.trim(), values.zipPostCode.trim().toUpperCase(), values.country);
function updateAddress(values: Address) {
PersonalDetails.updateAddress(
values.addressLine1?.trim() ?? '',
values.addressLine2?.trim() ?? '',
values.city.trim(),
values.state.trim(),
values?.zipPostCode?.trim().toUpperCase() ?? '',
values.country,
);
}

function AddressPage({privatePersonalDetails, route}) {
function AddressPage({privatePersonalDetails, route}: AddressPageProps) {
const styles = useThemeStyles();
usePrivatePersonalDetails();
const {translate} = useLocalize();
const address = useMemo(() => lodashGet(privatePersonalDetails, 'address') || {}, [privatePersonalDetails]);
const countryFromUrl = lodashGet(route, 'params.country');
const [currentCountry, setCurrentCountry] = useState(address.country);
const isLoadingPersonalDetails = lodashGet(privatePersonalDetails, 'isLoading', true);
const [street1, street2] = (address.street || '').split('\n');
const [state, setState] = useState(address.state);
const [city, setCity] = useState(address.city);
const [zipcode, setZipcode] = useState(address.zip);
const address = useMemo(() => privatePersonalDetails?.address, [privatePersonalDetails]);
const countryFromUrl = route.params?.country;
const [currentCountry, setCurrentCountry] = useState(address?.country);
const isLoadingPersonalDetails = privatePersonalDetails?.isLoading ?? true;
const [street1, street2] = (address?.street ?? '').split('\n');
const [state, setState] = useState(address?.state);
const [city, setCity] = useState(address?.city);
const [zipcode, setZipcode] = useState(address?.zip);

useEffect(() => {
if (!address) {
Expand All @@ -81,7 +62,7 @@ function AddressPage({privatePersonalDetails, route}) {
setZipcode(address.zip);
}, [address]);

const handleAddressChange = useCallback((value, key) => {
const handleAddressChange = useCallback((value: string, key: keyof Address) => {
if (key !== 'country' && key !== 'state' && key !== 'city' && key !== 'zipPostCode') {
return;
}
Expand Down Expand Up @@ -143,11 +124,9 @@ function AddressPage({privatePersonalDetails, route}) {
);
}

AddressPage.propTypes = propTypes;
AddressPage.defaultProps = defaultProps;
AddressPage.displayName = 'AddressPage';

export default withOnyx({
export default withOnyx<AddressPageProps, AddressPageOnyxProps>({
privatePersonalDetails: {
key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS,
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,46 +1,31 @@
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useMemo, useState} from 'react';
import _ from 'underscore';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import type {CountryData} from '@libs/searchCountryOptions';
import searchCountryOptions from '@libs/searchCountryOptions';
import StringUtils from '@libs/StringUtils';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {Route} from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';

const propTypes = {
/** Route from navigation */
route: PropTypes.shape({
/** Params from the route */
params: PropTypes.shape({
/** Currently selected country */
country: PropTypes.string,
type CountrySelectionPageProps = StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY>;

/** Route to navigate back after selecting a currency */
backTo: PropTypes.string,
}),
}).isRequired,

/** Navigation from react-navigation */
navigation: PropTypes.shape({
/** getState function retrieves the current navigation state from react-navigation's navigation property */
getState: PropTypes.func.isRequired,
}).isRequired,
};

function CountrySelectionPage({route, navigation}) {
function CountrySelectionPage({route, navigation}: CountrySelectionPageProps) {
const [searchValue, setSearchValue] = useState('');
const {translate} = useLocalize();
const currentCountry = lodashGet(route, 'params.country');
const currentCountry = route.params.country;

const countries = useMemo(
() =>
_.map(_.keys(CONST.ALL_COUNTRIES), (countryISO) => {
const countryName = translate(`allCountries.${countryISO}`);
Object.keys(CONST.ALL_COUNTRIES).map((countryISO) => {
const countryName = translate(`allCountries.${countryISO}` as TranslationPaths);
return {
value: countryISO,
keyForList: countryISO,
Expand All @@ -56,19 +41,18 @@ function CountrySelectionPage({route, navigation}) {
const headerMessage = searchValue.trim() && !searchResults.length ? translate('common.noResultsFound') : '';

const selectCountry = useCallback(
(option) => {
const backTo = lodashGet(route, 'params.backTo', '');

(option: CountryData) => {
const backTo = route.params.backTo ?? '';
// Check the navigation state and "backTo" parameter to decide navigation behavior
if (navigation.getState().routes.length === 1 && _.isEmpty(backTo)) {
if (navigation.getState().routes.length === 1 && !backTo) {
// If there is only one route and "backTo" is empty, go back in navigation
Navigation.goBack();
} else if (!_.isEmpty(backTo) && navigation.getState().routes.length === 1) {
} else if (!!backTo && navigation.getState().routes.length === 1) {
// If "backTo" is not empty and there is only one route, go back to the specific route defined in "backTo" with a country parameter
Navigation.goBack(`${route.params.backTo}?country=${option.value}`);
Navigation.goBack(`${route.params.backTo}?country=${option.value}` as Route);
} else {
// Otherwise, navigate to the specific route defined in "backTo" with a country parameter
Navigation.navigate(`${route.params.backTo}?country=${option.value}`);
Navigation.navigate(`${route.params.backTo}?country=${option.value}` as Route);
}
},
[route, navigation],
Expand All @@ -83,9 +67,9 @@ function CountrySelectionPage({route, navigation}) {
title={translate('common.country')}
shouldShowBackButton
onBackButtonPress={() => {
const backTo = lodashGet(route, 'params.backTo', '');
const backToRoute = backTo ? `${backTo}?country=${currentCountry}` : '';
Navigation.goBack(backToRoute);
const backTo = route.params.backTo ?? '';
const backToRoute = backTo ? (`${backTo}?country=${currentCountry}` as const) : '';
Navigation.goBack(backToRoute as Route);
}}
/>

Expand All @@ -105,6 +89,5 @@ function CountrySelectionPage({route, navigation}) {
}

CountrySelectionPage.displayName = 'CountrySelectionPage';
CountrySelectionPage.propTypes = propTypes;

export default CountrySelectionPage;
Loading

0 comments on commit 96b66b0

Please sign in to comment.