Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Travel] Bring back refactored AddressPage #43348

Merged
1 change: 1 addition & 0 deletions src/components/AddressForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ function AddressForm({
InputComponent={CountrySelector}
inputID={INPUT_IDS.COUNTRY}
value={country}
onValueChange={onAddressChanged}
shouldSaveDraft={shouldSaveDraft}
/>
</View>
Expand Down
30 changes: 22 additions & 8 deletions src/components/CountrySelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 Navigation from '@libs/Navigation/Navigation';
Expand Down Expand Up @@ -31,25 +32,38 @@ type CountrySelectorProps = {
function CountrySelector({errorText = '', value: countryCode, onInputChange = () => {}, onBlur}: CountrySelectorProps, ref: ForwardedRef<View>) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {country: countryFromUrl} = useGeographicalStateAndCountryFromRoute();

const title = countryCode ? translate(`allCountries.${countryCode}`) : '';
const countryTitleDescStyle = title.length === 0 ? styles.textNormal : null;

const didOpenContrySelector = useRef(false);
const isFocused = useIsFocused();
useEffect(() => {
if (!isFocused || !didOpenContrySelector.current) {
// 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) {
return;
}
didOpenContrySelector.current = false;
onBlur?.();
}, [isFocused, onBlur]);

useEffect(() => {
// This will cause the form to revalidate and remove any error related to country name
onInputChange(countryCode);
// 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
}, [countryCode]);
}, [countryFromUrl, isFocused, onBlur]);

return (
<MenuItemWithTopDescription
Expand Down
4 changes: 2 additions & 2 deletions src/components/StateSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {CONST as COMMON_CONST} from 'expensify-common';
import React, {useEffect, useRef} from 'react';
import type {ForwardedRef} from 'react';
import type {View} from 'react-native';
import useGeographicalStateFromRoute from '@hooks/useGeographicalStateFromRoute';
import useGeographicalStateAndCountryFromRoute from '@hooks/useGeographicalStateAndCountryFromRoute';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
Expand Down Expand Up @@ -43,7 +43,7 @@ function StateSelector(
) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const stateFromUrl = useGeographicalStateFromRoute();
const {state: stateFromUrl} = useGeographicalStateAndCountryFromRoute();

const didOpenStateSelector = useRef(false);
const isFocused = useIsFocused();
Expand Down
27 changes: 27 additions & 0 deletions src/hooks/useGeographicalStateAndCountryFromRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import {useRoute} from '@react-navigation/native';
import {CONST as COMMON_CONST} from 'expensify-common';
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<string, string>;

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,
};
}
23 changes: 0 additions & 23 deletions src/hooks/useGeographicalStateFromRoute.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.SETTINGS.PROFILE.TIMEZONE_SELECT]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/TimezoneSelectPage').default,
[SCREENS.SETTINGS.PROFILE.LEGAL_NAME]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/LegalNamePage').default,
[SCREENS.SETTINGS.PROFILE.DATE_OF_BIRTH]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default,
[SCREENS.SETTINGS.PROFILE.ADDRESS]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/AddressPage').default,
[SCREENS.SETTINGS.PROFILE.ADDRESS]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default,
[SCREENS.SETTINGS.PROFILE.ADDRESS_COUNTRY]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default,
[SCREENS.SETTINGS.PROFILE.ADDRESS_STATE]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/StateSelectionPage').default,
[SCREENS.SETTINGS.PROFILE.CONTACT_METHODS]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/Contacts/ContactMethodsPage').default,
Expand All @@ -197,7 +197,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.SETTINGS.APP_DOWNLOAD_LINKS]: () => require<ReactComponentModule>('../../../../pages/settings/AppDownloadLinks').default,
[SCREENS.SETTINGS.CONSOLE]: () => require<ReactComponentModule>('../../../../pages/settings/AboutPage/ConsolePage').default,
[SCREENS.SETTINGS.SHARE_LOG]: () => require<ReactComponentModule>('../../../../pages/settings/AboutPage/ShareLogPage').default,
[SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/AddressPage').default,
[SCREENS.SETTINGS.WALLET.CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').default,
[SCREENS.SETTINGS.WALLET.DOMAIN_CARD]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/ExpensifyCardPage').default,
[SCREENS.SETTINGS.WALLET.REPORT_VIRTUAL_CARD_FRAUD]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default,
[SCREENS.SETTINGS.WALLET.CARD_ACTIVATE]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/ActivatePhysicalCardPage').default,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,35 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React, {useCallback, useEffect, useMemo, useState} from 'react';
import React, {useCallback, useEffect, 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 AddressPageOnyxProps = {
type AddressPageProps = {
/** User's private personal details */
privatePersonalDetails: OnyxEntry<PrivatePersonalDetails>;
address?: Address;
/** Whether app is loading */
isLoadingApp: OnyxEntry<boolean>;
/** Function to call when address form is submitted */
updateAddress: (values: FormOnyxValues<typeof ONYXKEYS.FORMS.HOME_ADDRESS_FORM>) => void;
/** Title of address page */
title: string;
};

type AddressPageProps = StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.PROFILE.ADDRESS> & AddressPageOnyxProps;

/**
* Submit form to update user's first and last legal name
* @param values - form input values
*/
function updateAddress(values: FormOnyxValues<typeof ONYXKEYS.FORMS.HOME_ADDRESS_FORM>) {
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) {
function AddressPage({title, address, updateAddress, 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 countryFromUrl = CONST.ALL_COUNTRIES[countryFromUrlTemp as keyof typeof CONST.ALL_COUNTRIES] ? countryFromUrlTemp : '';
const stateFromUrl = useGeographicalStateFromRoute();
const {street, street2} = address ?? {};
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);
Expand All @@ -67,7 +42,8 @@ function AddressPage({privatePersonalDetails, route, isLoadingApp = true}: Addre
setCurrentCountry(address.country);
setCity(address.city);
setZipcode(address.zip);
}, [address]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [address?.state, address?.country, address?.city, address?.zip]);

const handleAddressChange = useCallback((value: unknown, key: unknown) => {
const addressPart = value as string;
Expand Down Expand Up @@ -97,27 +73,13 @@ function AddressPage({privatePersonalDetails, route, isLoadingApp = true}: Addre
setZipcode(addressPart);
}, []);

useEffect(() => {
if (!countryFromUrl) {
return;
}
handleAddressChange(countryFromUrl, 'country');
}, [countryFromUrl, handleAddressChange]);

useEffect(() => {
if (!stateFromUrl) {
return;
}
handleAddressChange(stateFromUrl, 'state');
}, [handleAddressChange, stateFromUrl]);

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={AddressPage.displayName}
>
<HeaderWithBackButton
title={translate('privatePersonalDetails.address')}
title={title}
shouldShowBackButton
onBackButtonPress={() => Navigation.goBack()}
/>
Expand All @@ -132,7 +94,7 @@ function AddressPage({privatePersonalDetails, route, isLoadingApp = true}: Addre
country={currentCountry}
onAddressChanged={handleAddressChange}
state={state}
street1={street1}
street1={street}
street2={street2}
zip={zipcode}
/>
Expand All @@ -143,11 +105,4 @@ function AddressPage({privatePersonalDetails, route, isLoadingApp = true}: Addre

AddressPage.displayName = 'AddressPage';

export default withOnyx<AddressPageProps, AddressPageOnyxProps>({
privatePersonalDetails: {
key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS,
},
isLoadingApp: {
key: ONYXKEYS.IS_LOADING_APP,
},
})(AddressPage);
export default AddressPage;
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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<PrivatePersonalDetails>;
/** Whether app is loading */
isLoadingApp: OnyxEntry<boolean>;
};

type PersonalAddressPageProps = StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.SETTINGS.PROFILE.ADDRESS> & PersonalAddressPageOnyxProps;

/**
* Submit form to update user's first and last legal name
* @param values - form input values
*/
function updateAddress(values: FormOnyxValues<typeof ONYXKEYS.FORMS.HOME_ADDRESS_FORM>) {
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 (
<AddressPage
address={address}
isLoadingApp={isLoadingApp}
updateAddress={updateAddress}
title={translate('privatePersonalDetails.address')}
/>
);
}

PersonalAddressPage.displayName = 'PersonalAddressPage';

export default withOnyx<PersonalAddressPageProps, PersonalAddressPageOnyxProps>({
privatePersonalDetails: {
key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS,
},
isLoadingApp: {
key: ONYXKEYS.IS_LOADING_APP,
},
})(PersonalAddressPage);
Loading
Loading