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 @@ -183,6 +183,7 @@ function AddressForm({
InputComponent={CountrySelector}
inputID={INPUT_IDS.COUNTRY}
value={country}
onValueChange={onAddressChanged}
shouldSaveDraft={shouldSaveDraft}
/>
</View>
Expand Down
28 changes: 24 additions & 4 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 type {MaybePhraseKey} from '@libs/Localize';
Expand Down Expand Up @@ -32,19 +33,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]);

// 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]);

useEffect(() => {
// This will cause the form to revalidate and remove any error related to country name
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 type {MaybePhraseKey} from '@libs/Localize';
Expand Down Expand Up @@ -44,7 +44,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 @@ -181,7 +181,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.SETTINGS.PROFILE.TIMEZONE_SELECT]: () => 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/AddressPage').default as React.ComponentType,
[SCREENS.SETTINGS.PROFILE.ADDRESS]: () => require('../../../../pages/settings/Profile/PersonalDetails/PersonalAddressPage').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,
Expand All @@ -195,7 +195,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.SETTINGS.APP_DOWNLOAD_LINKS]: () => 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/AddressPage').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.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,
Expand Down
107 changes: 107 additions & 0 deletions src/pages/AddressPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import React, {useCallback, useEffect, useState} from 'react';
import type {OnyxEntry} 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 useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {FormOnyxValues} from '@src/components/Form/types';
import type {Country} from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Address} from '@src/types/onyx/PrivatePersonalDetails';

type AddressPageProps = {
/** User's private personal details */
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;
};

function AddressPage({title, address, updateAddress, isLoadingApp = true}: AddressPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

// Check if country is valid
const {street, street2} = address ?? {};
const [currentCountry, setCurrentCountry] = useState(address?.country);
const [state, setState] = useState(address?.state);
const [city, setCity] = useState(address?.city);
const [zipcode, setZipcode] = useState(address?.zip);

useEffect(() => {
if (!address) {
return;
}
setState(address.state);
setCurrentCountry(address.country);
setCity(address.city);
setZipcode(address.zip);
}, [address]);

const handleAddressChange = useCallback((value: unknown, key: unknown) => {
const addressPart = value as string;
const addressPartKey = key as keyof Address;

if (addressPartKey !== 'country' && addressPartKey !== 'state' && addressPartKey !== 'city' && addressPartKey !== 'zipPostCode') {
return;
}
if (addressPartKey === 'country') {
setCurrentCountry(addressPart as Country | '');
setState('');
setCity('');
setZipcode('');
return;
}
if (addressPartKey === 'state') {
setState(addressPart);
setCity('');
setZipcode('');
return;
}
if (addressPartKey === 'city') {
setCity(addressPart);
setZipcode('');
return;
}
setZipcode(addressPart);
}, []);

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={AddressPage.displayName}
>
<HeaderWithBackButton
title={title}
shouldShowBackButton
onBackButtonPress={() => Navigation.goBack()}
/>
{isLoadingApp ? (
<FullscreenLoadingIndicator style={[styles.flex1, styles.pRelative]} />
) : (
<AddressForm
formID={ONYXKEYS.FORMS.HOME_ADDRESS_FORM}
onSubmit={updateAddress}
submitButtonText={translate('common.save')}
city={city}
country={currentCountry}
onAddressChanged={handleAddressChange}
state={state}
street1={street}
street2={street2}
zip={zipcode}
/>
)}
</ScreenWrapper>
);
}

AddressPage.displayName = 'AddressPage';

export default AddressPage;
Loading
Loading