From cc880813a9a7a08d1ff366d2ccfb7478348babc2 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 28 Feb 2024 15:09:15 +0100 Subject: [PATCH 1/2] ref: move Profile/PersonalDetails module to TS --- src/components/Form/FormWrapper.tsx | 2 +- src/components/Form/types.ts | 9 +- src/libs/Navigation/types.ts | 9 +- src/libs/PersonalDetailsUtils.ts | 4 +- src/libs/ValidationUtils.ts | 5 +- .../{AddressPage.js => AddressPage.tsx} | 81 ++++------ ...ectionPage.js => CountrySelectionPage.tsx} | 59 +++---- .../PersonalDetails/DateOfBirthPage.js | 110 ------------- .../PersonalDetails/DateOfBirthPage.tsx | 136 ++++++++++++++++ .../Profile/PersonalDetails/LegalNamePage.js | 145 ------------------ .../Profile/PersonalDetails/LegalNamePage.tsx | 136 ++++++++++++++++ src/types/onyx/PrivatePersonalDetails.ts | 5 + 12 files changed, 348 insertions(+), 353 deletions(-) rename src/pages/settings/Profile/PersonalDetails/{AddressPage.js => AddressPage.tsx} (62%) rename src/pages/settings/Profile/PersonalDetails/{CountrySelectionPage.js => CountrySelectionPage.tsx} (66%) delete mode 100644 src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.js create mode 100644 src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx delete mode 100644 src/pages/settings/Profile/PersonalDetails/LegalNamePage.js create mode 100644 src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx diff --git a/src/components/Form/FormWrapper.tsx b/src/components/Form/FormWrapper.tsx index 3a64a3df9af9..5615f3b87cfa 100644 --- a/src/components/Form/FormWrapper.tsx +++ b/src/components/Form/FormWrapper.tsx @@ -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} diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 37d0f730c9e9..569785156615 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -6,14 +6,15 @@ 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 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'; @@ -36,6 +37,7 @@ type ValidInputs = | typeof BusinessTypePicker | typeof StatePicker | typeof ValuePicker + | typeof DatePicker | typeof RadioButtons; type ValueTypeKey = 'string' | 'boolean' | 'date'; @@ -64,6 +66,9 @@ type InputComponentBaseProps = 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) => void; onPressOut?: (event: GestureResponderEvent) => void; @@ -121,6 +126,6 @@ type FormProps = { type InputRefs = Record>; -type FormInputErrors = Partial, TranslationPaths>>; +type FormInputErrors = Partial, MaybePhraseKey>>; export type {FormProps, ValidInputs, InputComponentValueProps, FormValue, ValueTypeKey, FormOnyxValues, FormOnyxKeys, FormInputErrors, InputRefs, InputComponentBaseProps, ValueTypeMap}; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a997e783deb0..e0bbbe95802f 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -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; }; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 55aee10e611a..1c517f42637f 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -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): 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); diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 0a46acbea102..669d10c4a1b8 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -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'; @@ -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> { +function getAgeRequirementError(date: string, minimumAge: number, maximumAge: number): MaybePhraseKey { const currentDate = startOfDay(new Date()); const testDate = parse(date, CONST.DATE.FNS_FORMAT_STRING, currentDate); @@ -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())); } diff --git a/src/pages/settings/Profile/PersonalDetails/AddressPage.js b/src/pages/settings/Profile/PersonalDetails/AddressPage.tsx similarity index 62% rename from src/pages/settings/Profile/PersonalDetails/AddressPage.js rename to src/pages/settings/Profile/PersonalDetails/AddressPage.tsx index 55d221085fcb..adc721ea0ea1 100644 --- a/src/pages/settings/Profile/PersonalDetails/AddressPage.js +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.tsx @@ -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'; @@ -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; }; -const defaultProps = { - privatePersonalDetails: { - address: { - street: '', - city: '', - state: '', - zip: '', - country: '', - }, - }, -}; +type AddressPageProps = StackScreenProps & 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) { @@ -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; } @@ -143,11 +124,9 @@ function AddressPage({privatePersonalDetails, route}) { ); } -AddressPage.propTypes = propTypes; -AddressPage.defaultProps = defaultProps; AddressPage.displayName = 'AddressPage'; -export default withOnyx({ +export default withOnyx({ privatePersonalDetails: { key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, }, diff --git a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.js b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx similarity index 66% rename from src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.js rename to src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx index d8327041538d..968ffb9276f1 100644 --- a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.js +++ b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx @@ -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; - /** 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, @@ -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], @@ -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); }} /> @@ -105,6 +89,5 @@ function CountrySelectionPage({route, navigation}) { } CountrySelectionPage.displayName = 'CountrySelectionPage'; -CountrySelectionPage.propTypes = propTypes; -export default CountrySelectionPage; +export default CountrySelectionPage; \ No newline at end of file diff --git a/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.js b/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.js deleted file mode 100644 index 943d9fe0bab7..000000000000 --- a/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.js +++ /dev/null @@ -1,110 +0,0 @@ -import {subYears} from 'date-fns'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import DatePicker from '@components/DatePicker'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import Navigation from '@libs/Navigation/Navigation'; -import * as ValidationUtils from '@libs/ValidationUtils'; -import * as PersonalDetails from '@userActions/PersonalDetails'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/DateOfBirthForm'; - -const propTypes = { - /* Onyx Props */ - - /** User's private personal details */ - privatePersonalDetails: PropTypes.shape({ - dob: PropTypes.string, - }), - - ...withLocalizePropTypes, -}; - -const defaultProps = { - privatePersonalDetails: { - dob: '', - }, -}; - -function DateOfBirthPage({translate, privatePersonalDetails}) { - const styles = useThemeStyles(); - usePrivatePersonalDetails(); - const isLoadingPersonalDetails = lodashGet(privatePersonalDetails, 'isLoading', true); - - /** - * @param {Object} values - * @param {String} values.dob - date of birth - * @returns {Object} - An object containing the errors for each inputID - */ - const validate = useCallback((values) => { - const requiredFields = ['dob']; - const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields); - - const minimumAge = CONST.DATE_BIRTH.MIN_AGE; - const maximumAge = CONST.DATE_BIRTH.MAX_AGE; - const dateError = ValidationUtils.getAgeRequirementError(values.dob, minimumAge, maximumAge); - - if (values.dob && dateError) { - errors.dob = dateError; - } - - return errors; - }, []); - - return ( - - Navigation.goBack()} - /> - {isLoadingPersonalDetails ? ( - - ) : ( - - - - )} - - ); -} - -DateOfBirthPage.propTypes = propTypes; -DateOfBirthPage.defaultProps = defaultProps; -DateOfBirthPage.displayName = 'DateOfBirthPage'; - -export default compose( - withLocalize, - withOnyx({ - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, - }), -)(DateOfBirthPage); diff --git a/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx b/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx new file mode 100644 index 000000000000..5e8e4ea493a0 --- /dev/null +++ b/src/pages/settings/Profile/PersonalDetails/DateOfBirthPage.tsx @@ -0,0 +1,136 @@ +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import * as PersonalDetails from '@userActions/PersonalDetails'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/LegalNameForm'; +import type {PrivatePersonalDetails} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; + +type LegalNamePageOnyxProps = { + /** User's private personal details */ + privatePersonalDetails: OnyxEntry; +}; + +type LegalNamePageProps = LegalNamePageOnyxProps; + +const updateLegalName = (values: PrivatePersonalDetails) => { + PersonalDetails.updateLegalName(values.legalFirstName?.trim() ?? '', values.legalLastName?.trim() ?? ''); +}; + +function LegalNamePage({privatePersonalDetails}: LegalNamePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + usePrivatePersonalDetails(); + const legalFirstName = privatePersonalDetails?.legalFirstName ?? ''; + const legalLastName = privatePersonalDetails?.legalLastName ?? ''; + const isLoadingPersonalDetails = privatePersonalDetails?.isLoading ?? true; + + const validate = useCallback((values: FormOnyxValues) => { + const errors: Errors = {}; + + if (typeof values.legalFirstName === 'string') { + if (!ValidationUtils.isValidLegalName(values.legalFirstName)) { + ErrorUtils.addErrorMessage(errors, 'legalFirstName', 'privatePersonalDetails.error.hasInvalidCharacter'); + } else if (!values.legalFirstName) { + errors.legalFirstName = 'common.error.fieldRequired'; + } else if (values.legalFirstName.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'legalFirstName', [ + 'common.error.characterLimitExceedCounter', + {length: values.legalFirstName.length, limit: CONST.TITLE_CHARACTER_LIMIT}, + ]); + } + if (ValidationUtils.doesContainReservedWord(values.legalFirstName, CONST.DISPLAY_NAME.RESERVED_NAMES)) { + ErrorUtils.addErrorMessage(errors, 'legalFirstName', 'personalDetails.error.containsReservedWord'); + } + } + + if (typeof values.legalLastName === 'string') { + if (!ValidationUtils.isValidLegalName(values.legalLastName)) { + ErrorUtils.addErrorMessage(errors, 'legalLastName', 'privatePersonalDetails.error.hasInvalidCharacter'); + } else if (!values.legalLastName) { + errors.legalLastName = 'common.error.fieldRequired'; + } else if (values.legalLastName.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'legalLastName', ['common.error.characterLimitExceedCounter', {length: values.legalLastName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); + } + if (ValidationUtils.doesContainReservedWord(values.legalLastName, CONST.DISPLAY_NAME.RESERVED_NAMES)) { + ErrorUtils.addErrorMessage(errors, 'legalLastName', 'personalDetails.error.containsReservedWord'); + } + } + + return errors; + }, []); + + return ( + + Navigation.goBack()} + /> + {isLoadingPersonalDetails ? ( + + ) : ( + + + + + + + + + )} + + ); +} + +LegalNamePage.displayName = 'LegalNamePage'; + +export default withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, +})(LegalNamePage); diff --git a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js deleted file mode 100644 index 0e81ea5194c1..000000000000 --- a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.js +++ /dev/null @@ -1,145 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import FormProvider from '@components/Form/FormProvider'; -import InputWrapper from '@components/Form/InputWrapper'; -import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import TextInput from '@components/TextInput'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import * as ValidationUtils from '@libs/ValidationUtils'; -import * as PersonalDetails from '@userActions/PersonalDetails'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import INPUT_IDS from '@src/types/form/LegalNameForm'; - -const propTypes = { - /* Onyx Props */ - - /** User's private personal details */ - privatePersonalDetails: PropTypes.shape({ - legalFirstName: PropTypes.string, - legalLastName: PropTypes.string, - }), - - ...withLocalizePropTypes, -}; - -const defaultProps = { - privatePersonalDetails: { - legalFirstName: '', - legalLastName: '', - }, -}; - -const updateLegalName = (values) => { - PersonalDetails.updateLegalName(values.legalFirstName.trim(), values.legalLastName.trim()); -}; - -function LegalNamePage(props) { - const styles = useThemeStyles(); - usePrivatePersonalDetails(); - const legalFirstName = lodashGet(props.privatePersonalDetails, 'legalFirstName', ''); - const legalLastName = lodashGet(props.privatePersonalDetails, 'legalLastName', ''); - const isLoadingPersonalDetails = lodashGet(props.privatePersonalDetails, 'isLoading', true); - - const validate = useCallback((values) => { - const errors = {}; - - if (!ValidationUtils.isValidLegalName(values.legalFirstName)) { - ErrorUtils.addErrorMessage(errors, 'legalFirstName', 'privatePersonalDetails.error.hasInvalidCharacter'); - } else if (_.isEmpty(values.legalFirstName)) { - errors.legalFirstName = 'common.error.fieldRequired'; - } else if (values.legalFirstName.length > CONST.TITLE_CHARACTER_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'legalFirstName', ['common.error.characterLimitExceedCounter', {length: values.legalFirstName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); - } - if (ValidationUtils.doesContainReservedWord(values.legalFirstName, CONST.DISPLAY_NAME.RESERVED_NAMES)) { - ErrorUtils.addErrorMessage(errors, 'legalFirstName', 'personalDetails.error.containsReservedWord'); - } - - if (!ValidationUtils.isValidLegalName(values.legalLastName)) { - ErrorUtils.addErrorMessage(errors, 'legalLastName', 'privatePersonalDetails.error.hasInvalidCharacter'); - } else if (_.isEmpty(values.legalLastName)) { - errors.legalLastName = 'common.error.fieldRequired'; - } else if (values.legalLastName.length > CONST.TITLE_CHARACTER_LIMIT) { - ErrorUtils.addErrorMessage(errors, 'legalLastName', ['common.error.characterLimitExceedCounter', {length: values.legalLastName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); - } - if (ValidationUtils.doesContainReservedWord(values.legalLastName, CONST.DISPLAY_NAME.RESERVED_NAMES)) { - ErrorUtils.addErrorMessage(errors, 'legalLastName', 'personalDetails.error.containsReservedWord'); - } - - return errors; - }, []); - - return ( - - Navigation.goBack()} - /> - {isLoadingPersonalDetails ? ( - - ) : ( - - - - - - - - - )} - - ); -} - -LegalNamePage.propTypes = propTypes; -LegalNamePage.defaultProps = defaultProps; -LegalNamePage.displayName = 'LegalNamePage'; - -export default compose( - withLocalize, - withOnyx({ - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, - }), -)(LegalNamePage); diff --git a/src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx new file mode 100644 index 000000000000..5e8e4ea493a0 --- /dev/null +++ b/src/pages/settings/Profile/PersonalDetails/LegalNamePage.tsx @@ -0,0 +1,136 @@ +import React, {useCallback} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import * as ValidationUtils from '@libs/ValidationUtils'; +import * as PersonalDetails from '@userActions/PersonalDetails'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/LegalNameForm'; +import type {PrivatePersonalDetails} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; + +type LegalNamePageOnyxProps = { + /** User's private personal details */ + privatePersonalDetails: OnyxEntry; +}; + +type LegalNamePageProps = LegalNamePageOnyxProps; + +const updateLegalName = (values: PrivatePersonalDetails) => { + PersonalDetails.updateLegalName(values.legalFirstName?.trim() ?? '', values.legalLastName?.trim() ?? ''); +}; + +function LegalNamePage({privatePersonalDetails}: LegalNamePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + usePrivatePersonalDetails(); + const legalFirstName = privatePersonalDetails?.legalFirstName ?? ''; + const legalLastName = privatePersonalDetails?.legalLastName ?? ''; + const isLoadingPersonalDetails = privatePersonalDetails?.isLoading ?? true; + + const validate = useCallback((values: FormOnyxValues) => { + const errors: Errors = {}; + + if (typeof values.legalFirstName === 'string') { + if (!ValidationUtils.isValidLegalName(values.legalFirstName)) { + ErrorUtils.addErrorMessage(errors, 'legalFirstName', 'privatePersonalDetails.error.hasInvalidCharacter'); + } else if (!values.legalFirstName) { + errors.legalFirstName = 'common.error.fieldRequired'; + } else if (values.legalFirstName.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'legalFirstName', [ + 'common.error.characterLimitExceedCounter', + {length: values.legalFirstName.length, limit: CONST.TITLE_CHARACTER_LIMIT}, + ]); + } + if (ValidationUtils.doesContainReservedWord(values.legalFirstName, CONST.DISPLAY_NAME.RESERVED_NAMES)) { + ErrorUtils.addErrorMessage(errors, 'legalFirstName', 'personalDetails.error.containsReservedWord'); + } + } + + if (typeof values.legalLastName === 'string') { + if (!ValidationUtils.isValidLegalName(values.legalLastName)) { + ErrorUtils.addErrorMessage(errors, 'legalLastName', 'privatePersonalDetails.error.hasInvalidCharacter'); + } else if (!values.legalLastName) { + errors.legalLastName = 'common.error.fieldRequired'; + } else if (values.legalLastName.length > CONST.TITLE_CHARACTER_LIMIT) { + ErrorUtils.addErrorMessage(errors, 'legalLastName', ['common.error.characterLimitExceedCounter', {length: values.legalLastName.length, limit: CONST.TITLE_CHARACTER_LIMIT}]); + } + if (ValidationUtils.doesContainReservedWord(values.legalLastName, CONST.DISPLAY_NAME.RESERVED_NAMES)) { + ErrorUtils.addErrorMessage(errors, 'legalLastName', 'personalDetails.error.containsReservedWord'); + } + } + + return errors; + }, []); + + return ( + + Navigation.goBack()} + /> + {isLoadingPersonalDetails ? ( + + ) : ( + + + + + + + + + )} + + ); +} + +LegalNamePage.displayName = 'LegalNamePage'; + +export default withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, +})(LegalNamePage); diff --git a/src/types/onyx/PrivatePersonalDetails.ts b/src/types/onyx/PrivatePersonalDetails.ts index 780e3f71b61d..5a9dae0a5523 100644 --- a/src/types/onyx/PrivatePersonalDetails.ts +++ b/src/types/onyx/PrivatePersonalDetails.ts @@ -5,6 +5,9 @@ type Address = { state: string; zip: string; country: string; + zipPostCode?: string; + addressLine1?: string; + addressLine2?: string; }; type PrivatePersonalDetails = { @@ -21,3 +24,5 @@ type PrivatePersonalDetails = { }; export default PrivatePersonalDetails; + +export type {Address}; From a1369db76dda7ca2de3ad6999c597ad4b71dc275 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 28 Feb 2024 16:22:29 +0100 Subject: [PATCH 2/2] fix: prettier --- .../settings/Profile/PersonalDetails/CountrySelectionPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx index 968ffb9276f1..f07d560ab454 100644 --- a/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/CountrySelectionPage.tsx @@ -90,4 +90,4 @@ function CountrySelectionPage({route, navigation}: CountrySelectionPageProps) { CountrySelectionPage.displayName = 'CountrySelectionPage'; -export default CountrySelectionPage; \ No newline at end of file +export default CountrySelectionPage;