diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4d27c5f5e8cb..fc1e531b1f1d 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -546,4 +546,4 @@ type Route = RouteIsPlainString extends true ? never : AllRoutes; type HybridAppRoute = (typeof HYBRID_APP_ROUTES)[keyof typeof HYBRID_APP_ROUTES]; -export type {Route, HybridAppRoute}; +export type {Route, HybridAppRoute, AllRoutes}; diff --git a/src/components/AddressSearch/index.tsx b/src/components/AddressSearch/index.tsx index 89e87eeebe54..8ad26e5a7c46 100644 --- a/src/components/AddressSearch/index.tsx +++ b/src/components/AddressSearch/index.tsx @@ -272,7 +272,7 @@ function AddressSearch( const renderHeaderComponent = () => ( <> - {predefinedPlaces.length > 0 && ( + {(predefinedPlaces?.length ?? 0) > 0 && ( <> {/* This will show current location button in list if there are some recent destinations */} {shouldShowCurrentLocationButton && ( @@ -339,7 +339,7 @@ function AddressSearch( fetchDetails suppressDefaultStyles enablePoweredByContainer={false} - predefinedPlaces={predefinedPlaces} + predefinedPlaces={predefinedPlaces ?? undefined} listEmptyComponent={listEmptyComponent} listLoaderComponent={listLoader} renderHeaderComponent={renderHeaderComponent} @@ -348,7 +348,7 @@ function AddressSearch( const subtitle = data.isPredefinedPlace ? data.description : data.structured_formatting.secondary_text; return ( - {title && {title}} + {!!title && {title}} {subtitle} ); @@ -398,10 +398,10 @@ function AddressSearch( if (inputID) { onInputChange?.(text); } else { - onInputChange({street: text}); + onInputChange?.({street: text}); } // If the text is empty and we have no predefined places, we set displayListViewBorder to false to prevent UI flickering - if (!text && !predefinedPlaces.length) { + if (!text && !predefinedPlaces?.length) { setDisplayListViewBorder(false); } }, diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts index 9b4254a9bc45..e115d4f697b2 100644 --- a/src/components/AddressSearch/types.ts +++ b/src/components/AddressSearch/types.ts @@ -20,6 +20,8 @@ type RenamedInputKeysProps = { lat: string; lng: string; zipCode: string; + address?: string; + country?: string; }; type OnPressProps = { @@ -59,7 +61,7 @@ type AddressSearchProps = { defaultValue?: string; /** A callback function when the value of this field has changed */ - onInputChange: (value: string | number | RenamedInputKeysProps | StreetValue, key?: string) => void; + onInputChange?: (value: string | number | RenamedInputKeysProps | StreetValue, key?: string) => void; /** A callback function when an address has been auto-selected */ onPress?: (props: OnPressProps) => void; @@ -74,7 +76,7 @@ type AddressSearchProps = { canUseCurrentLocation?: boolean; /** A list of predefined places that can be shown when the user isn't searching for something */ - predefinedPlaces?: Place[]; + predefinedPlaces?: Place[] | null; /** A map of inputID key names */ renamedInputKeys: RenamedInputKeysProps; diff --git a/src/hooks/useLocationBias.ts b/src/hooks/useLocationBias.ts index b95ffbb57e9d..e18aba4a907c 100644 --- a/src/hooks/useLocationBias.ts +++ b/src/hooks/useLocationBias.ts @@ -1,15 +1,18 @@ import {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {UserLocation} from '@src/types/onyx'; +import type {WaypointCollection} from '@src/types/onyx/Transaction'; /** * Construct the rectangular boundary based on user location and waypoints */ -export default function useLocationBias(allWaypoints: Record, userLocation?: {latitude: number; longitude: number}) { +export default function useLocationBias(allWaypoints: WaypointCollection, userLocation?: OnyxEntry) { return useMemo(() => { const hasFilledWaypointCount = Object.values(allWaypoints).some((waypoint) => Object.keys(waypoint).length > 0); // If there are no filled wayPoints and if user's current location cannot be retrieved, // it is futile to arrive at a biased location. Let's return if (!hasFilledWaypointCount && userLocation === undefined) { - return null; + return undefined; } // Gather the longitudes and latitudes from filled waypoints. @@ -29,8 +32,8 @@ export default function useLocationBias(allWaypoints: Record; + reportID: string; + backTo: Routes | undefined; + action: ValueOf; + pageIndex: string; + }; [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: { action: ValueOf; iouType: ValueOf; diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 56cf1c475812..0a46acbea102 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -37,7 +37,11 @@ function validateCardNumber(value: string): boolean { /** * Validating that this is a valid address (PO boxes are not allowed) */ -function isValidAddress(value: string): boolean { +function isValidAddress(value: FormValue): boolean { + if (typeof value !== 'string') { + return false; + } + if (!CONST.REGEX.ANY_VALUE.test(value) || value.match(CONST.REGEX.EMOJIS)) { return false; } diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 1d9af01f2fa0..5b178104d7c7 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -1,6 +1,7 @@ import {isEqual} from 'lodash'; import lodashClone from 'lodash/clone'; import lodashHas from 'lodash/has'; +import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type {GetRouteForDraftParams, GetRouteParams} from '@libs/API/parameters'; @@ -106,7 +107,7 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp } } -function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: boolean) { +function removeWaypoint(transaction: OnyxEntry, currentIndex: string, isDraft?: boolean) { // Index comes from the route params and is a string const index = Number(currentIndex); const existingWaypoints = transaction?.comment?.waypoints ?? {}; @@ -134,9 +135,10 @@ function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: // to remove nested keys while also preserving other object keys // Doing a deep clone of the transaction to avoid mutating the original object and running into a cache issue when using Onyx.set let newTransaction: Transaction = { - ...transaction, + // eslint-disable-next-line @typescript-eslint/non-nullable-type-assertion-style + ...(transaction as Transaction), comment: { - ...transaction.comment, + ...transaction?.comment, waypoints: reIndexedWaypoints, }, // We want to reset the amount only for draft transactions (when creating the request). @@ -164,10 +166,10 @@ function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: }; } if (isDraft) { - Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction.transactionID}`, newTransaction); + Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transaction?.transactionID}`, newTransaction); return; } - Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, newTransaction); + Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction?.transactionID}`, newTransaction); } function getOnyxDataForRouteRequest(transactionID: string, isDraft = false): OnyxData { diff --git a/src/pages/iou/MoneyRequestWaypointPage.js b/src/pages/iou/MoneyRequestWaypointPage.tsx similarity index 52% rename from src/pages/iou/MoneyRequestWaypointPage.js rename to src/pages/iou/MoneyRequestWaypointPage.tsx index 2f8b8b9cc729..c21aae7cf063 100644 --- a/src/pages/iou/MoneyRequestWaypointPage.js +++ b/src/pages/iou/MoneyRequestWaypointPage.tsx @@ -1,39 +1,21 @@ -import PropTypes from 'prop-types'; +import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; +import type {MoneyRequestNavigatorParamList} from '@libs/Navigation/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; import IOURequestStepWaypoint from './request/step/IOURequestStepWaypoint'; -const propTypes = { - /** The transactionID of this request */ - transactionID: PropTypes.string, - - /** Route params */ - route: PropTypes.shape({ - params: PropTypes.shape({ - /** IOU type */ - iouType: PropTypes.string, - - /** Index of the waypoint being edited */ - waypointIndex: PropTypes.string, - }), - }), -}; - -const defaultProps = { - transactionID: '', - route: { - params: { - iouType: '', - waypointIndex: '', - }, - }, +type MoneyRequestWaypointPageOnyxProps = { + transactionID: string | undefined; }; +type MoneyRequestWaypointPageProps = StackScreenProps & MoneyRequestWaypointPageOnyxProps; // This component is responsible for grabbing the transactionID from the IOU key // You can't use Onyx props in the withOnyx mapping, so we need to set up and access the transactionID here, and then pass it down so that WaypointEditor can subscribe to the transaction. -function MoneyRequestWaypointPage({transactionID, route}) { +function MoneyRequestWaypointPage({transactionID = '', route}: MoneyRequestWaypointPageProps) { return ( + // @ts-expect-error TODO: Remove this once withFullTransactionOrNotFound(https://github.com/Expensify/App/issues/36123) is migrated to TypeScript. iou && iou.transactionID}, + +export default withOnyx({ + transactionID: {key: ONYXKEYS.IOU, selector: (iou) => iou?.transactionID}, })(MoneyRequestWaypointPage); diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.js b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx similarity index 64% rename from src/pages/iou/request/step/IOURequestStepWaypoint.js rename to src/pages/iou/request/step/IOURequestStepWaypoint.tsx index 93a87baa0481..eee6da9e87ef 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.js +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.tsx @@ -1,91 +1,69 @@ import {useNavigation} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; +import type {TextInput} from 'react-native'; +import type {Place} from 'react-native-google-places-autocomplete'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import AddressSearch from '@components/AddressSearch'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import ConfirmModal from '@components/ConfirmModal'; import FormProvider from '@components/Form/FormProvider'; import InputWrapperWithRef from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; -import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import useLocationBias from '@hooks/useLocationBias'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -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 Transaction from '@userActions/Transaction'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Route as Routes} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Waypoint} from '@src/types/onyx/Transaction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import withFullTransactionOrNotFound from './withFullTransactionOrNotFound'; import withWritableReportOrNotFound from './withWritableReportOrNotFound'; -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: IOURequestStepRoutePropTypes.isRequired, +type IOURequestStepWaypointOnyxProps = { + /** List of recent waypoints */ + recentWaypoints: OnyxEntry; - /* Onyx props */ - /** The optimistic transaction for this request */ - transaction: transactionPropTypes, - - /* Current location coordinates of the user */ - userLocation: PropTypes.shape({ - /** Latitude of the location */ - latitude: PropTypes.number, - - /** Longitude of the location */ - longitude: PropTypes.number, - }), - - /** Recent waypoints that the user has selected */ - recentWaypoints: PropTypes.arrayOf( - PropTypes.shape({ - /** The name of the location */ - name: PropTypes.string, - - /** A description of the location (usually the address) */ - description: PropTypes.string, - - /** Data required by the google auto complete plugin to know where to put the markers on the map */ - geometry: PropTypes.shape({ - /** Data about the location */ - location: PropTypes.shape({ - /** Latitude of the location */ - lat: PropTypes.number, - - /** Longitude of the location */ - lng: PropTypes.number, - }), - }), - }), - ), + userLocation: OnyxEntry; }; -const defaultProps = { - recentWaypoints: [], - transaction: {}, - userLocation: undefined, -}; +type IOURequestStepWaypointProps = { + route: { + params: { + iouType: ValueOf; + transactionID: string; + reportID: string; + backTo: Routes | undefined; + action: ValueOf; + pageIndex: string; + }; + }; + transaction: OnyxEntry; +} & IOURequestStepWaypointOnyxProps; function IOURequestStepWaypoint({ - recentWaypoints, route: { params: {action, backTo, iouType, pageIndex, reportID, transactionID}, }, transaction, + recentWaypoints = [], userLocation, -}) { +}: IOURequestStepWaypointProps) { const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false); @@ -93,12 +71,12 @@ function IOURequestStepWaypoint({ const isFocused = navigation.isFocused(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); - const textInput = useRef(null); + const textInput = useRef(null); const parsedWaypointIndex = parseInt(pageIndex, 10); - const allWaypoints = lodashGet(transaction, 'comment.waypoints', {}); - const currentWaypoint = lodashGet(allWaypoints, `waypoint${pageIndex}`, {}); - const waypointCount = _.size(allWaypoints); - const filledWaypointCount = _.size(_.filter(allWaypoints, (waypoint) => !_.isEmpty(waypoint))); + const allWaypoints = transaction?.comment.waypoints ?? {}; + const currentWaypoint = allWaypoints[`waypoint${pageIndex}`] ?? {}; + const waypointCount = Object.keys(allWaypoints).length; + const filledWaypointCount = Object.values(allWaypoints).filter((waypoint) => !isEmptyObject(waypoint)).length; const waypointDescriptionKey = useMemo(() => { switch (parsedWaypointIndex) { @@ -112,16 +90,16 @@ function IOURequestStepWaypoint({ }, [parsedWaypointIndex, waypointCount]); const locationBias = useLocationBias(allWaypoints, userLocation); - const waypointAddress = lodashGet(currentWaypoint, 'address', ''); - // Hide the menu when there is only start and finish waypoint or the current waypoint is empty - const shouldShowThreeDotsButton = waypointCount > 2 && waypointAddress; + const waypointAddress = currentWaypoint.address ?? ''; + // Hide the menu when there is only start and finish waypoint + const shouldShowThreeDotsButton = waypointCount > 2 && !!waypointAddress; const shouldDisableEditor = isFocused && (Number.isNaN(parsedWaypointIndex) || parsedWaypointIndex < 0 || parsedWaypointIndex > waypointCount || (filledWaypointCount < 2 && parsedWaypointIndex >= waypointCount)); - const validate = (values) => { + const validate = (values: FormOnyxValues<'waypointForm'>): Partial> => { const errors = {}; - const waypointValue = values[`waypoint${pageIndex}`] || ''; + const waypointValue = values[`waypoint${pageIndex}`] ?? ''; if (isOffline && waypointValue !== '' && !ValidationUtils.isValidAddress(waypointValue)) { ErrorUtils.addErrorMessage(errors, `waypoint${pageIndex}`, 'bankAccount.error.address'); } @@ -135,11 +113,10 @@ function IOURequestStepWaypoint({ return errors; }; - const saveWaypoint = (waypoint) => Transaction.saveWaypoint(transactionID, pageIndex, waypoint, action === CONST.IOU.ACTION.CREATE); - - const submit = (values) => { - const waypointValue = values[`waypoint${pageIndex}`] || ''; + const saveWaypoint = (waypoint: FormOnyxValues<'waypointForm'>) => Transaction.saveWaypoint(transactionID, pageIndex, waypoint, action === CONST.IOU.ACTION.CREATE); + const submit = (values: FormOnyxValues<'waypointForm'>) => { + const waypointValue = values[`waypoint${pageIndex}`] ?? ''; // Allows letting you set a waypoint to an empty value if (waypointValue === '') { Transaction.removeWaypoint(transaction, pageIndex, true); @@ -149,10 +126,8 @@ function IOURequestStepWaypoint({ // Therefore, we're going to save the waypoint as just the address, and the lat/long will be filled in on the backend if (isOffline && waypointValue) { const waypoint = { - lat: null, - lng: null, address: waypointValue, - name: values.name || null, + name: values.name, }; saveWaypoint(waypoint); } @@ -167,19 +142,14 @@ function IOURequestStepWaypoint({ Navigation.goBack(ROUTES.MONEY_REQUEST_DISTANCE_TAB.getRoute(iouType)); }; - /** - * @param {Object} values - * @param {String} values.lat - * @param {String} values.lng - * @param {String} values.address - */ - const selectWaypoint = (values) => { + const selectWaypoint = (values: Waypoint) => { const waypoint = { lat: values.lat, lng: values.lng, address: values.address, - name: values.name || null, + name: values.name, }; + Transaction.saveWaypoint(transactionID, pageIndex, waypoint, action === CONST.IOU.ACTION.CREATE); if (backTo) { Navigation.goBack(backTo); @@ -191,7 +161,7 @@ function IOURequestStepWaypoint({ return ( textInput.current && textInput.current.focus()} + onEntryTransitionEnd={() => textInput.current?.focus()} shouldEnableMaxHeight testID={IOURequestStepWaypoint.displayName} > @@ -240,7 +210,9 @@ function IOURequestStepWaypoint({ locationBias={locationBias} canUseCurrentLocation inputID={`waypoint${pageIndex}`} - ref={(e) => (textInput.current = e)} + ref={(e: HTMLElement | null) => { + textInput.current = e as unknown as TextInput; + }} hint={!isOffline ? 'distance.errors.selectSuggestedAddress' : ''} containerStyles={[styles.mt4]} label={translate('distance.address')} @@ -249,14 +221,14 @@ function IOURequestStepWaypoint({ maxInputLength={CONST.FORM_CHARACTER_LIMIT} renamedInputKeys={{ address: `waypoint${pageIndex}`, - city: null, - country: null, - street: null, - street2: null, - zipCode: null, - lat: null, - lng: null, - state: null, + city: '', + country: '', + street: '', + street2: '', + zipCode: '', + lat: '', + lng: '', + state: '', }} predefinedPlaces={recentWaypoints} resultTypes="" @@ -269,32 +241,32 @@ function IOURequestStepWaypoint({ } IOURequestStepWaypoint.displayName = 'IOURequestStepWaypoint'; -IOURequestStepWaypoint.propTypes = propTypes; -IOURequestStepWaypoint.defaultProps = defaultProps; -export default compose( - withWritableReportOrNotFound, - withFullTransactionOrNotFound, - withOnyx({ - userLocation: { - key: ONYXKEYS.USER_LOCATION, - }, - recentWaypoints: { - key: ONYXKEYS.NVP_RECENT_WAYPOINTS, +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepWaypointWithWritableReportOrNotFound = withWritableReportOrNotFound(IOURequestStepWaypoint); +// eslint-disable-next-line rulesdir/no-negated-variables +const IOURequestStepWaypointWithFullTransactionOrNotFound = withFullTransactionOrNotFound(IOURequestStepWaypointWithWritableReportOrNotFound); - // Only grab the most recent 5 waypoints because that's all that is shown in the UI. This also puts them into the format of data - // that the google autocomplete component expects for it's "predefined places" feature. - selector: (waypoints) => - _.map(waypoints ? waypoints.slice(0, 5) : [], (waypoint) => ({ - name: waypoint.name, - description: waypoint.address, - geometry: { - location: { - lat: waypoint.lat, - lng: waypoint.lng, - }, +export default withOnyx({ + userLocation: { + key: ONYXKEYS.USER_LOCATION, + }, + recentWaypoints: { + key: ONYXKEYS.NVP_RECENT_WAYPOINTS, + + // Only grab the most recent 5 waypoints because that's all that is shown in the UI. This also puts them into the format of data + // that the google autocomplete component expects for it's "predefined places" feature. + selector: (waypoints) => + (waypoints ? waypoints.slice(0, 5) : []).map((waypoint) => ({ + name: waypoint.name, + description: waypoint.address ?? '', + geometry: { + location: { + lat: waypoint.lat ?? 0, + lng: waypoint.lng ?? 0, }, - })), - }, - }), -)(IOURequestStepWaypoint); + }, + })), + }, + // @ts-expect-error TODO: Remove this once withFullTransactionOrNotFound (https://github.com/Expensify/App/issues/36123) is migrated to TypeScript. +})(IOURequestStepWaypointWithFullTransactionOrNotFound); diff --git a/src/types/onyx/RecentWaypoint.ts b/src/types/onyx/RecentWaypoint.ts index 097aed3be916..55232f7ef71d 100644 --- a/src/types/onyx/RecentWaypoint.ts +++ b/src/types/onyx/RecentWaypoint.ts @@ -3,13 +3,13 @@ type RecentWaypoint = { name?: string; /** The full address of the waypoint */ - address: string; + address?: string; /** The lattitude of the waypoint */ - lat: number; + lat?: number; /** The longitude of the waypoint */ - lng: number; + lng?: number; }; export default RecentWaypoint; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index ee6a7f2d0b4c..1324bb9c6902 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -16,6 +16,24 @@ type Waypoint = { /** The longitude of the waypoint */ lng?: number; + + /** Address city */ + city?: string; + + /** Address state */ + state?: string; + + /** Address zip code */ + zipCode?: string; + + /** Address country */ + country?: string; + + /** Address street line 1 */ + street?: string; + + /** Address street line 2 */ + street2?: string; }; type WaypointCollection = Record;