From aed0318c1eb49008c69f0c4b12f75f32bef6b538 Mon Sep 17 00:00:00 2001 From: Vit Horacek <36083550+mountiny@users.noreply.github.com> Date: Tue, 28 May 2024 15:55:50 +0200 Subject: [PATCH] Revert "[Travel] [Refactor] Create a new shared component for AddressPage" --- src/components/AddressForm.tsx | 1 - src/components/CountrySelector.tsx | 28 +---- src/components/StateSelector.tsx | 4 +- ...useGeographicalStateAndCountryFromRoute.ts | 27 ---- src/hooks/useGeographicalStateFromRoute.ts | 23 ++++ .../ModalStackNavigators/index.tsx | 4 +- .../Profile/PersonalDetails}/AddressPage.tsx | 70 +++++++++-- .../PersonalDetails/PersonalAddressPage.tsx | 61 --------- .../workspace/WorkspaceProfileAddressPage.tsx | 117 ++++++++++++++---- 9 files changed, 182 insertions(+), 153 deletions(-) delete mode 100644 src/hooks/useGeographicalStateAndCountryFromRoute.ts create mode 100644 src/hooks/useGeographicalStateFromRoute.ts rename src/pages/{ => settings/Profile/PersonalDetails}/AddressPage.tsx (57%) delete mode 100644 src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx diff --git a/src/components/AddressForm.tsx b/src/components/AddressForm.tsx index f716548f55d6..296ecce7d092 100644 --- a/src/components/AddressForm.tsx +++ b/src/components/AddressForm.tsx @@ -180,7 +180,6 @@ function AddressForm({ InputComponent={CountrySelector} inputID={INPUT_IDS.COUNTRY} value={country} - onValueChange={onAddressChanged} shouldSaveDraft={shouldSaveDraft} /> diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx index b8558c6bd92b..002c0c6d4b0a 100644 --- a/src/components/CountrySelector.tsx +++ b/src/components/CountrySelector.tsx @@ -2,7 +2,6 @@ import {useIsFocused} from '@react-navigation/native'; import React, {forwardRef, useEffect, useRef} from 'react'; import type {ForwardedRef} from 'react'; import type {View} from 'react-native'; -import useGeographicalStateAndCountryFromRoute from '@hooks/useGeographicalStateAndCountryFromRoute'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import type {MaybePhraseKey} from '@libs/Localize'; @@ -33,7 +32,6 @@ type CountrySelectorProps = { function CountrySelector({errorText = '', value: countryCode, onInputChange = () => {}, onBlur}: CountrySelectorProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const {country: countryFromUrl} = useGeographicalStateAndCountryFromRoute(); const title = countryCode ? translate(`allCountries.${countryCode}`) : ''; const countryTitleDescStyle = title.length === 0 ? styles.textNormal : null; @@ -41,30 +39,12 @@ function CountrySelector({errorText = '', value: countryCode, onInputChange = () const didOpenContrySelector = useRef(false); const isFocused = useIsFocused(); useEffect(() => { - // Check if the country selector was opened and no value was selected, triggering onBlur to display an error - if (isFocused && didOpenContrySelector.current) { - didOpenContrySelector.current = false; - if (!countryFromUrl) { - onBlur?.(); - } - } - - // If no country is selected from the URL, exit the effect early to avoid further processing. - if (!countryFromUrl) { + if (!isFocused || !didOpenContrySelector.current) { return; } - - // If a country is selected, invoke `onInputChange` to update the form and clear any validation errors related to the country selection. - if (onInputChange) { - onInputChange(countryFromUrl); - } - - // Clears the `country` parameter from the URL to ensure the component country is driven by the parent component rather than URL parameters. - // This helps prevent issues where the component might not update correctly if the country is controlled by both the parent and the URL. - Navigation.setParams({country: undefined}); - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [countryFromUrl, isFocused, onBlur]); + didOpenContrySelector.current = false; + onBlur?.(); + }, [isFocused, onBlur]); useEffect(() => { // This will cause the form to revalidate and remove any error related to country name diff --git a/src/components/StateSelector.tsx b/src/components/StateSelector.tsx index 046d5a00fb2b..8cae007679ff 100644 --- a/src/components/StateSelector.tsx +++ b/src/components/StateSelector.tsx @@ -3,7 +3,7 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import React, {useEffect, useRef} from 'react'; import type {ForwardedRef} from 'react'; import type {View} from 'react-native'; -import useGeographicalStateAndCountryFromRoute from '@hooks/useGeographicalStateAndCountryFromRoute'; +import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import type {MaybePhraseKey} from '@libs/Localize'; @@ -44,7 +44,7 @@ function StateSelector( ) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const {state: stateFromUrl} = useGeographicalStateAndCountryFromRoute(); + const stateFromUrl = useGeographicalStateFromRoute(); const didOpenStateSelector = useRef(false); const isFocused = useIsFocused(); diff --git a/src/hooks/useGeographicalStateAndCountryFromRoute.ts b/src/hooks/useGeographicalStateAndCountryFromRoute.ts deleted file mode 100644 index c29ea585eebf..000000000000 --- a/src/hooks/useGeographicalStateAndCountryFromRoute.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {useRoute} from '@react-navigation/native'; -import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; -import CONST from '@src/CONST'; - -type State = keyof typeof COMMON_CONST.STATES; -type Country = keyof typeof CONST.ALL_COUNTRIES; -type StateAndCountry = {state?: State; country?: Country}; - -/** - * Extracts the 'state' and 'country' query parameters from the route/ url and validates it against COMMON_CONST.STATES and CONST.ALL_COUNTRIES. - * Example 1: Url: https://new.expensify.com/settings/profile/address?state=MO Returns: state=MO - * Example 2: Url: https://new.expensify.com/settings/profile/address?state=ASDF Returns: state=undefined - * Example 3: Url: https://new.expensify.com/settings/profile/address Returns: state=undefined - * Example 4: Url: https://new.expensify.com/settings/profile/address?state=MO-hash-a12341 Returns: state=MO - * Similarly for country parameter. - */ -export default function useGeographicalStateAndCountryFromRoute(stateParamName = 'state', countryParamName = 'country'): StateAndCountry { - const routeParams = useRoute().params as Record; - - const stateFromUrlTemp = routeParams?.[stateParamName] as string | undefined; - const countryFromUrlTemp = routeParams?.[countryParamName] as string | undefined; - - return { - state: COMMON_CONST.STATES[stateFromUrlTemp as State]?.stateISO, - country: Object.keys(CONST.ALL_COUNTRIES).find((country) => country === countryFromUrlTemp) as Country, - }; -} diff --git a/src/hooks/useGeographicalStateFromRoute.ts b/src/hooks/useGeographicalStateFromRoute.ts new file mode 100644 index 000000000000..13936ee78f5b --- /dev/null +++ b/src/hooks/useGeographicalStateFromRoute.ts @@ -0,0 +1,23 @@ +import {useRoute} from '@react-navigation/native'; +import type {ParamListBase, RouteProp} from '@react-navigation/native'; +import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; + +type CustomParamList = ParamListBase & Record>; +type State = keyof typeof COMMON_CONST.STATES; + +/** + * Extracts the 'state' (default) query parameter from the route/ url and validates it against COMMON_CONST.STATES, returning its ISO code or `undefined`. + * Example 1: Url: https://new.expensify.com/settings/profile/address?state=MO Returns: MO + * Example 2: Url: https://new.expensify.com/settings/profile/address?state=ASDF Returns: undefined + * Example 3: Url: https://new.expensify.com/settings/profile/address Returns: undefined + * Example 4: Url: https://new.expensify.com/settings/profile/address?state=MO-hash-a12341 Returns: MO + */ +export default function useGeographicalStateFromRoute(stateParamName = 'state'): State | undefined { + const route = useRoute>(); + const stateFromUrlTemp = route.params?.[stateParamName] as string | undefined; + + if (!stateFromUrlTemp) { + return; + } + return COMMON_CONST.STATES[stateFromUrlTemp as State]?.stateISO; +} diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 127b886a6a54..462145e56907 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -179,7 +179,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/Profile/TimezoneSelectPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.LEGAL_NAME]: () => require('../../../../pages/settings/Profile/PersonalDetails/LegalNamePage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH]: () => require('../../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default as React.ComponentType, - [SCREENS.SETTINGS.PROFILE.ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default as React.ComponentType, + [SCREENS.SETTINGS.PROFILE.ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/AddressPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: () => require('../../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.ADDRESS_STATE]: () => require('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: () => require('../../../../pages/settings/Profile/Contacts/ContactMethodsPage').default as React.ComponentType, @@ -193,7 +193,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/settings/AppDownloadLinks').default as React.ComponentType, [SCREENS.SETTINGS.CONSOLE]: () => require('../../../../pages/settings/AboutPage/ConsolePage').default as React.ComponentType, [SCREENS.SETTINGS.SHARE_LOG]: () => require('../../../../pages/settings/AboutPage/ShareLogPage').default as React.ComponentType, - [SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/AddressPage').default as React.ComponentType, [SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: () => require('../../../../pages/settings/Wallet/ExpensifyCardPage').default as React.ComponentType, [SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: () => require('../../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default as React.ComponentType, [SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: () => require('../../../../pages/settings/Wallet/ActivatePhysicalCardPage').default as React.ComponentType, diff --git a/src/pages/AddressPage.tsx b/src/pages/settings/Profile/PersonalDetails/AddressPage.tsx similarity index 57% rename from src/pages/AddressPage.tsx rename to src/pages/settings/Profile/PersonalDetails/AddressPage.tsx index 90711ebbab92..91a8b94537ab 100644 --- a/src/pages/AddressPage.tsx +++ b/src/pages/settings/Profile/PersonalDetails/AddressPage.tsx @@ -1,35 +1,60 @@ -import React, {useCallback, useEffect, useState} from 'react'; +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'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; +import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute'; import useLocalize from '@hooks/useLocalize'; 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 type {FormOnyxValues} from '@src/components/Form/types'; +import CONST from '@src/CONST'; import type {Country} from '@src/CONST'; 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'; -type AddressPageProps = { +type AddressPageOnyxProps = { /** User's private personal details */ - address?: Address; + privatePersonalDetails: OnyxEntry; /** Whether app is loading */ isLoadingApp: OnyxEntry; - /** Function to call when address form is submitted */ - updateAddress: (values: FormOnyxValues) => void; - /** Title of address page */ - title: string; }; -function AddressPage({title, address, updateAddress, isLoadingApp = true}: AddressPageProps) { +type AddressPageProps = StackScreenProps & AddressPageOnyxProps; + +/** + * Submit form to update user's first and last legal name + * @param values - form input values + */ +function updateAddress(values: FormOnyxValues) { + PersonalDetails.updateAddress( + values.addressLine1?.trim() ?? '', + values.addressLine2?.trim() ?? '', + values.city.trim(), + values.state.trim(), + values?.zipPostCode?.trim().toUpperCase() ?? '', + values.country, + ); +} + +function AddressPage({privatePersonalDetails, route, isLoadingApp = true}: AddressPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const address = useMemo(() => privatePersonalDetails?.address, [privatePersonalDetails]); + const countryFromUrlTemp = route?.params?.country; // Check if country is valid - const {street, street2} = address ?? {}; + const countryFromUrl = CONST.ALL_COUNTRIES[countryFromUrlTemp as keyof typeof CONST.ALL_COUNTRIES] ? countryFromUrlTemp : ''; + const stateFromUrl = useGeographicalStateFromRoute(); const [currentCountry, setCurrentCountry] = useState(address?.country); + 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); @@ -72,13 +97,27 @@ function AddressPage({title, address, updateAddress, isLoadingApp = true}: Addre setZipcode(addressPart); }, []); + useEffect(() => { + if (!countryFromUrl) { + return; + } + handleAddressChange(countryFromUrl, 'country'); + }, [countryFromUrl, handleAddressChange]); + + useEffect(() => { + if (!stateFromUrl) { + return; + } + handleAddressChange(stateFromUrl, 'state'); + }, [handleAddressChange, stateFromUrl]); + return ( Navigation.goBack()} /> @@ -93,7 +132,7 @@ function AddressPage({title, address, updateAddress, isLoadingApp = true}: Addre country={currentCountry} onAddressChanged={handleAddressChange} state={state} - street1={street} + street1={street1} street2={street2} zip={zipcode} /> @@ -104,4 +143,11 @@ function AddressPage({title, address, updateAddress, isLoadingApp = true}: Addre AddressPage.displayName = 'AddressPage'; -export default AddressPage; +export default withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, + isLoadingApp: { + key: ONYXKEYS.IS_LOADING_APP, + }, +})(AddressPage); diff --git a/src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx b/src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx deleted file mode 100644 index 85402137fe6d..000000000000 --- a/src/pages/settings/Profile/PersonalDetails/PersonalAddressPage.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useMemo} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import useLocalize from '@hooks/useLocalize'; -import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import AddressPage from '@pages/AddressPage'; -import * as PersonalDetails from '@userActions/PersonalDetails'; -import type {FormOnyxValues} from '@src/components/Form/types'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; -import type {PrivatePersonalDetails} from '@src/types/onyx'; - -type PersonalAddressPageOnyxProps = { - /** User's private personal details */ - privatePersonalDetails: OnyxEntry; - /** Whether app is loading */ - isLoadingApp: OnyxEntry; -}; - -type PersonalAddressPageProps = StackScreenProps & PersonalAddressPageOnyxProps; - -/** - * Submit form to update user's first and last legal name - * @param values - form input values - */ -function updateAddress(values: FormOnyxValues) { - PersonalDetails.updateAddress( - values.addressLine1?.trim() ?? '', - values.addressLine2?.trim() ?? '', - values.city.trim(), - values.state.trim(), - values?.zipPostCode?.trim().toUpperCase() ?? '', - values.country, - ); -} - -function PersonalAddressPage({privatePersonalDetails, isLoadingApp = true}: PersonalAddressPageProps) { - const {translate} = useLocalize(); - const address = useMemo(() => privatePersonalDetails?.address, [privatePersonalDetails]); - - return ( - - ); -} - -PersonalAddressPage.displayName = 'PersonalAddressPage'; - -export default withOnyx({ - privatePersonalDetails: { - key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, - }, - isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP, - }, -})(PersonalAddressPage); diff --git a/src/pages/workspace/WorkspaceProfileAddressPage.tsx b/src/pages/workspace/WorkspaceProfileAddressPage.tsx index 79e851b3b751..02c58d4af244 100644 --- a/src/pages/workspace/WorkspaceProfileAddressPage.tsx +++ b/src/pages/workspace/WorkspaceProfileAddressPage.tsx @@ -1,14 +1,21 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useMemo} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import AddressForm from '@components/AddressForm'; import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import AddressPage from '@pages/AddressPage'; import {updateAddress} from '@userActions/Policy'; -import type ONYXKEYS from '@src/ONYXKEYS'; +import type {Country} from '@src/CONST'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {Address} from '@src/types/onyx/PrivatePersonalDetails'; +import type {CompanyAddress} from '@src/types/onyx/Policy'; import type {WithPolicyProps} from './withPolicy'; import withPolicy from './withPolicy'; @@ -16,21 +23,18 @@ type WorkspaceProfileAddressPagePolicyProps = WithPolicyProps; type WorkspaceProfileAddressPageProps = StackScreenProps & WorkspaceProfileAddressPagePolicyProps; -function WorkspaceProfileAddressPage({policy}: WorkspaceProfileAddressPageProps) { +function WorkspaceProfileAddressPage({policy, route}: WorkspaceProfileAddressPageProps) { + const styles = useThemeStyles(); const {translate} = useLocalize(); - const address: Address = useMemo(() => { - const tempAddress = policy?.address; - const [street1, street2] = (tempAddress?.addressStreet ?? '').split('\n'); - const result = { - street: street1?.trim() ?? '', - street2: street2?.trim() ?? '', - city: tempAddress?.city?.trim() ?? '', - state: tempAddress?.state?.trim() ?? '', - zip: tempAddress?.zipCode?.trim().toUpperCase() ?? '', - country: tempAddress?.country ?? '', - }; - return result; - }, [policy]); + const address = useMemo(() => policy?.address, [policy]); + const [currentCountry, setCurrentCountry] = useState(address?.country); + const [[street1, street2], setStreets] = useState((address?.addressStreet ?? '').split('\n')); + const [state, setState] = useState(address?.state); + const [city, setCity] = useState(address?.city); + const [zipcode, setZipcode] = useState(address?.zipCode); + + const countryFromUrlTemp = route?.params?.country; + const countryFromUrl = CONST.ALL_COUNTRIES[countryFromUrlTemp as keyof typeof CONST.ALL_COUNTRIES] ? countryFromUrlTemp : ''; const updatePolicyAddress = (values: FormOnyxValues) => { if (!policy) { @@ -46,13 +50,78 @@ function WorkspaceProfileAddressPage({policy}: WorkspaceProfileAddressPageProps) Navigation.goBack(); }; + const handleAddressChange = useCallback((value: unknown, key: unknown) => { + const countryValue = value as Country | ''; + const addressKey = key as keyof CompanyAddress; + + if (addressKey !== 'country' && addressKey !== 'state' && addressKey !== 'city' && addressKey !== 'zipCode') { + return; + } + if (addressKey === 'country') { + setCurrentCountry(countryValue); + setState(''); + setCity(''); + setZipcode(''); + return; + } + if (addressKey === 'state') { + setState(countryValue); + setCity(''); + setZipcode(''); + return; + } + if (addressKey === 'city') { + setCity(countryValue); + setZipcode(''); + return; + } + setZipcode(countryValue); + }, []); + + useEffect(() => { + if (!address) { + return; + } + setStreets((address?.addressStreet ?? '').split('\n')); + setState(address.state); + setCurrentCountry(address.country); + setCity(address.city); + setZipcode(address.zipCode); + }, [address]); + + useEffect(() => { + if (!countryFromUrl) { + return; + } + handleAddressChange(countryFromUrl, 'country'); + }, [countryFromUrl, handleAddressChange]); + return ( - + + Navigation.goBack()} + /> + + {translate('workspace.editor.addressContext')} + + + ); }