From c4f93a3515cc8d25f9d7a718035714afa4158b68 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 25 Aug 2023 11:12:09 -0600 Subject: [PATCH 1/8] Add the onyx key --- src/ONYXKEYS.ts | 4 ++++ src/types/onyx/RecentWaypoints.ts | 12 ++++++++++++ src/types/onyx/index.ts | 4 ++-- 3 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 src/types/onyx/RecentWaypoints.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d4d2ab1f90a6..2df7dd0b5a76 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -106,6 +106,9 @@ const ONYXKEYS = { /** The NVP with the last payment method used per policy */ NVP_LAST_PAYMENT_METHOD: 'nvp_lastPaymentMethod', + /** This NVP holds to most recent waypoints that a person has used when creating a distance request */ + NVP_RECENT_WAYPOINTS: 'expensify_recentWaypoints', + /** Does this user have push notifications enabled for this device? */ PUSH_NOTIFICATIONS_ENABLED: 'pushNotificationsEnabled', @@ -315,6 +318,7 @@ type OnyxValues = { [ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge; [ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string; [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: Record; + [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoints[]; [ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED]: boolean; [ONYXKEYS.PLAID_DATA]: OnyxTypes.PlaidData; [ONYXKEYS.IS_PLAID_DISABLED]: boolean; diff --git a/src/types/onyx/RecentWaypoints.ts b/src/types/onyx/RecentWaypoints.ts new file mode 100644 index 000000000000..75780ef861e5 --- /dev/null +++ b/src/types/onyx/RecentWaypoints.ts @@ -0,0 +1,12 @@ +type RecentWaypoints = { + /** The full address of the waypoint */ + address: string; + + /** The lattitude of the waypoint */ + lat: number; + + /** The longitude of the waypoint */ + lng: number; +}; + +export default RecentWaypoints; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index e7ab360551c5..8d48eccf1464 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -32,7 +32,6 @@ import ReimbursementAccountDraft from './ReimbursementAccountDraft'; import WalletTransfer from './WalletTransfer'; import ReceiptModal from './ReceiptModal'; import MapboxAccessToken from './MapboxAccessToken'; - import Download from './Download'; import PolicyMember from './PolicyMember'; import Policy from './Policy'; @@ -41,8 +40,8 @@ import ReportAction from './ReportAction'; import ReportActionReactions from './ReportActionReactions'; import SecurityGroup from './SecurityGroup'; import Transaction from './Transaction'; - import Form, {AddDebitCardForm} from './Form'; +import RecentWaypoints from './RecentWaypoints'; export type { Account, @@ -89,4 +88,5 @@ export type { Transaction, Form, AddDebitCardForm, + RecentWaypoints, }; From c50feeba29775e120f0cec62ba0c1a2a0f106b54 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 25 Aug 2023 13:11:41 -0600 Subject: [PATCH 2/8] Display recent searches --- src/components/AddressSearch/index.js | 22 ++++++++++++++++++++++ src/pages/iou/WaypointEditor.js | 27 +++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 204333474849..55a62f5a4560 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -61,6 +61,26 @@ const propTypes = { /** Should address search be limited to results in the USA */ isLimitedToUSA: PropTypes.bool, + /** A list of recent search results that can be shown when the user isn't searching for something */ + recentSearchSuggestions: PropTypes.arrayOf( + PropTypes.shape({ + /** 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({ + /** Lattitude of the location */ + lat: PropTypes.string, + + /** Longitude of the location */ + lng: PropTypes.string, + }), + }), + }), + ), + /** A map of inputID key names */ renamedInputKeys: PropTypes.shape({ street: PropTypes.string, @@ -102,6 +122,7 @@ const defaultProps = { lng: 'addressLng', }, maxInputLength: undefined, + recentSearchSuggestions: [], }; // Do not convert to class component! It's been tried before and presents more challenges than it's worth. @@ -250,6 +271,7 @@ function AddressSearch(props) { fetchDetails suppressDefaultStyles enablePoweredByContainer={false} + predefinedPlaces={props.recentSearchSuggestions.length ? props.recentSearchSuggestions : null} ListEmptyComponent={ props.network.isOffline ? null : ( {props.translate('common.noResultsFound')} diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js index a3f8f40f5372..c24a9fe553eb 100644 --- a/src/pages/iou/WaypointEditor.js +++ b/src/pages/iou/WaypointEditor.js @@ -107,6 +107,25 @@ function WaypointEditor({transactionID, route: {params: {iouType = '', waypointI Navigation.goBack(ROUTES.getMoneyRequestDistanceTabRoute(iouType)); }; + const recentWaypoints = [ + {address: '0', lat: 0, lng: 0}, + {address: '1', lat: 0, lng: 0}, + {address: '2', lat: 0, lng: 0}, + {address: '3', lat: 0, lng: 0}, + {address: '4', lat: 0, lng: 0}, + ]; + const recentSearchSuggestions = _.map(recentWaypoints, (waypoint) => { + return { + description: waypoint.address, + geometry: { + location: { + lat: waypoint.lat, + lng: waypoint.lng, + }, + }, + }; + }); + return ( @@ -169,5 +189,12 @@ export default compose( key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION}${props.transactionID}`, selector: (transaction) => (transaction ? {transactionID: transaction.transactionID, comment: {waypoints: lodashGet(transaction, 'comment.waypoints')}} : null), }, + + recentWaypoints: { + key: ONYXKEYS.NVP_RECENT_WAYPOINTS, + + // Only grab the most recent 5 waypoints because that's all that is shown in the UI + selector: (waypoints) => (waypoints && waypoints.length ? waypoints.slice(0, 5) : []), + }, }), )(WaypointEditor); From f9dcecf829cb3df3b8614cfd4492e0cad38d0418 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 25 Aug 2023 13:48:15 -0600 Subject: [PATCH 3/8] Cleanup the data passed to autocomplete --- src/components/AddressSearch/index.js | 35 ++++++++++++++++++--------- src/pages/iou/WaypointEditor.js | 35 ++++++++++----------------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 55a62f5a4560..fe666369a062 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -61,8 +61,8 @@ const propTypes = { /** Should address search be limited to results in the USA */ isLimitedToUSA: PropTypes.bool, - /** A list of recent search results that can be shown when the user isn't searching for something */ - recentSearchSuggestions: PropTypes.arrayOf( + /** A list of predefined places that can be shown when the user isn't searching for something */ + predefinedPlaces: PropTypes.arrayOf( PropTypes.shape({ /** A description of the location (usually the address) */ description: PropTypes.string, @@ -72,10 +72,10 @@ const propTypes = { /** Data about the location */ location: PropTypes.shape({ /** Lattitude of the location */ - lat: PropTypes.string, + lat: PropTypes.number, /** Longitude of the location */ - lng: PropTypes.string, + lng: PropTypes.number, }), }), }), @@ -122,7 +122,7 @@ const defaultProps = { lng: 'addressLng', }, maxInputLength: undefined, - recentSearchSuggestions: [], + predefinedPlaces: [], }; // Do not convert to class component! It's been tried before and presents more challenges than it's worth. @@ -143,6 +143,19 @@ function AddressSearch(props) { const saveLocationDetails = (autocompleteData, details) => { const addressComponents = details.address_components; if (!addressComponents) { + // When there are details, but no address_components, this indicates that some predefined options have been passed + // to this component which don't match the usual properties coming from auto-complete. In that case, only a limited + // amount of data massaging needs to happen for what the parent expects to get from this function. + if (_.size(details)) { + props.onPress({ + address: lodashGet(details, 'description', ''), + lat: lodashGet(details, 'geometry.location.lat', 0), + lng: lodashGet(details, 'geometry.location.lng', 0), + }); + + // After we select an option, we set displayListViewBorder to false to prevent UI flickering + setDisplayListViewBorder(false); + } return; } @@ -244,6 +257,9 @@ function AddressSearch(props) { } props.onPress(values); + + // After we select an option, we set displayListViewBorder to false to prevent UI flickering + setDisplayListViewBorder(false); }; return ( @@ -271,18 +287,13 @@ function AddressSearch(props) { fetchDetails suppressDefaultStyles enablePoweredByContainer={false} - predefinedPlaces={props.recentSearchSuggestions.length ? props.recentSearchSuggestions : null} + predefinedPlaces={props.predefinedPlaces.length ? props.predefinedPlaces : null} ListEmptyComponent={ props.network.isOffline ? null : ( {props.translate('common.noResultsFound')} ) } - onPress={(data, details) => { - saveLocationDetails(data, details); - - // After we select an option, we set displayListViewBorder to false to prevent UI flickering - setDisplayListViewBorder(false); - }} + onPress={saveLocationDetails} query={query} requestUrl={{ useOnPlatform: 'all', diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js index c24a9fe553eb..760c3281360f 100644 --- a/src/pages/iou/WaypointEditor.js +++ b/src/pages/iou/WaypointEditor.js @@ -107,25 +107,6 @@ function WaypointEditor({transactionID, route: {params: {iouType = '', waypointI Navigation.goBack(ROUTES.getMoneyRequestDistanceTabRoute(iouType)); }; - const recentWaypoints = [ - {address: '0', lat: 0, lng: 0}, - {address: '1', lat: 0, lng: 0}, - {address: '2', lat: 0, lng: 0}, - {address: '3', lat: 0, lng: 0}, - {address: '4', lat: 0, lng: 0}, - ]; - const recentSearchSuggestions = _.map(recentWaypoints, (waypoint) => { - return { - description: waypoint.address, - geometry: { - location: { - lat: waypoint.lat, - lng: waypoint.lng, - }, - }, - }; - }); - return ( @@ -193,8 +174,18 @@ export default compose( recentWaypoints: { key: ONYXKEYS.NVP_RECENT_WAYPOINTS, - // Only grab the most recent 5 waypoints because that's all that is shown in the UI - selector: (waypoints) => (waypoints && waypoints.length ? waypoints.slice(0, 5) : []), + // 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.length ? waypoints.slice(0, 5) : [], (waypoint) => ({ + description: waypoint.address, + geometry: { + location: { + lat: waypoint.lat, + lng: waypoint.lng, + }, + }, + })), }, }), )(WaypointEditor); From f58407a6b4c384738ee86718df9e563bdf62f529 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 25 Aug 2023 13:50:52 -0600 Subject: [PATCH 4/8] Fix prop references --- src/components/AddressSearch/index.js | 2 +- src/pages/iou/WaypointEditor.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index fe666369a062..9d89ac7e1dbc 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -287,7 +287,7 @@ function AddressSearch(props) { fetchDetails suppressDefaultStyles enablePoweredByContainer={false} - predefinedPlaces={props.predefinedPlaces.length ? props.predefinedPlaces : null} + predefinedPlaces={props.predefinedPlaces} ListEmptyComponent={ props.network.isOffline ? null : ( {props.translate('common.noResultsFound')} diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js index 760c3281360f..12177c956918 100644 --- a/src/pages/iou/WaypointEditor.js +++ b/src/pages/iou/WaypointEditor.js @@ -53,7 +53,7 @@ const defaultProps = { transaction: {}, }; -function WaypointEditor({transactionID, route: {params: {iouType = '', waypointIndex = ''} = {}} = {}, network, translate, transaction}) { +function WaypointEditor({transactionID, route: {params: {iouType = '', waypointIndex = ''} = {}} = {}, network, translate, transaction, recentWaypoints}) { const textInput = useRef(null); const currentWaypoint = lodashGet(transaction, `comment.waypoints.waypoint${waypointIndex}`, {}); const waypointAddress = lodashGet(currentWaypoint, 'address', ''); @@ -151,7 +151,7 @@ function WaypointEditor({transactionID, route: {params: {iouType = '', waypointI lng: null, state: null, }} - predefinedPlaces={props.recentWaypoints} + predefinedPlaces={recentWaypoints} /> From 2c9ffa50f585a19cc97c5dacefe11e0b3aab4164 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Fri, 25 Aug 2023 14:10:08 -0600 Subject: [PATCH 5/8] Store recent waypoints optimistically --- src/libs/actions/Transaction.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/libs/actions/Transaction.js b/src/libs/actions/Transaction.js index 02927bf7d111..8d83dd229031 100644 --- a/src/libs/actions/Transaction.js +++ b/src/libs/actions/Transaction.js @@ -5,6 +5,12 @@ import ONYXKEYS from '../../ONYXKEYS'; import * as CollectionUtils from '../CollectionUtils'; import * as API from '../API'; +let recentWaypoints = []; +Onyx.connect({ + key: ONYXKEYS.NVP_RECENT_WAYPOINTS, + callback: (val) => (recentWaypoints = val || []), +}); + const allTransactions = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.TRANSACTION, @@ -87,6 +93,11 @@ function saveWaypoint(transactionID, index, waypoint) { }, }, }); + const recentWaypointAlreadyExists = _.find(recentWaypoints, (recentWaypoint) => recentWaypoint.address === waypoint.address); + if (!recentWaypointAlreadyExists) { + recentWaypoints.unshift(waypoint); + Onyx.merge(ONYXKEYS.NVP_RECENT_WAYPOINTS, recentWaypoints.slice(0, 5)); + } } function removeWaypoint(transactionID, currentIndex) { From e791e0281e3cc724516ba9d1cbffb70f4d537686 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 28 Aug 2023 09:03:42 -0600 Subject: [PATCH 6/8] Import underscore --- src/pages/iou/WaypointEditor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js index 12177c956918..2dbc457b930f 100644 --- a/src/pages/iou/WaypointEditor.js +++ b/src/pages/iou/WaypointEditor.js @@ -1,4 +1,5 @@ import React, {useRef} from 'react'; +import _ from 'underscore'; import lodashGet from 'lodash/get'; import {View} from 'react-native'; import PropTypes from 'prop-types'; From b818dba927d96c3684994eb66c75b15f3b6d7ac1 Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 28 Aug 2023 10:11:46 -0600 Subject: [PATCH 7/8] Revert onPress handler logic --- src/components/AddressSearch/index.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 9d89ac7e1dbc..262f656fcee1 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -152,9 +152,6 @@ function AddressSearch(props) { lat: lodashGet(details, 'geometry.location.lat', 0), lng: lodashGet(details, 'geometry.location.lng', 0), }); - - // After we select an option, we set displayListViewBorder to false to prevent UI flickering - setDisplayListViewBorder(false); } return; } @@ -257,9 +254,6 @@ function AddressSearch(props) { } props.onPress(values); - - // After we select an option, we set displayListViewBorder to false to prevent UI flickering - setDisplayListViewBorder(false); }; return ( @@ -293,7 +287,12 @@ function AddressSearch(props) { {props.translate('common.noResultsFound')} ) } - onPress={saveLocationDetails} + onPress={(data, details) => { + saveLocationDetails(data, details); + + // After we select an option, we set displayListViewBorder to false to prevent UI flickering + setDisplayListViewBorder(false); + }} query={query} requestUrl={{ useOnPlatform: 'all', From ff8e2279ec0bd89aefc84c71e7ab654cf4e9624a Mon Sep 17 00:00:00 2001 From: Tim Golen Date: Mon, 28 Aug 2023 10:43:24 -0600 Subject: [PATCH 8/8] Clone waypoints and remove paranoid logic --- src/libs/actions/Transaction.js | 5 +++-- src/pages/iou/WaypointEditor.js | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Transaction.js b/src/libs/actions/Transaction.js index 8d83dd229031..1dad0219db1a 100644 --- a/src/libs/actions/Transaction.js +++ b/src/libs/actions/Transaction.js @@ -95,8 +95,9 @@ function saveWaypoint(transactionID, index, waypoint) { }); const recentWaypointAlreadyExists = _.find(recentWaypoints, (recentWaypoint) => recentWaypoint.address === waypoint.address); if (!recentWaypointAlreadyExists) { - recentWaypoints.unshift(waypoint); - Onyx.merge(ONYXKEYS.NVP_RECENT_WAYPOINTS, recentWaypoints.slice(0, 5)); + const clonedWaypoints = _.clone(recentWaypoints); + clonedWaypoints.unshift(waypoint); + Onyx.merge(ONYXKEYS.NVP_RECENT_WAYPOINTS, clonedWaypoints.slice(0, 5)); } } diff --git a/src/pages/iou/WaypointEditor.js b/src/pages/iou/WaypointEditor.js index 2dbc457b930f..c0c621e353cb 100644 --- a/src/pages/iou/WaypointEditor.js +++ b/src/pages/iou/WaypointEditor.js @@ -178,7 +178,7 @@ export default compose( // 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.length ? waypoints.slice(0, 5) : [], (waypoint) => ({ + _.map(waypoints ? waypoints.slice(0, 5) : [], (waypoint) => ({ description: waypoint.address, geometry: { location: {