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

New places api support #47085

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
04ea3a8
new places api support
rojiphil Aug 7, 2024
6425853
pass fields as property
rojiphil Aug 8, 2024
4836de6
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Aug 11, 2024
4692371
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Sep 19, 2024
43d5dba
update rectangle object for locationbias
rojiphil Sep 21, 2024
67cc635
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Sep 21, 2024
9a75de7
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Sep 27, 2024
0e52dc9
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Sep 29, 2024
ae0ef58
using longText and shortText + prettier fix
rojiphil Oct 3, 2024
b396dd8
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Oct 3, 2024
0c07e99
use newer version for the autocomplete library
rojiphil Oct 4, 2024
4379632
updated package-lock
rojiphil Oct 4, 2024
18d79b0
lint fix
rojiphil Oct 4, 2024
8f05d04
ts fix
rojiphil Oct 4, 2024
0ad4459
using useOnyx instead of withOnyx - eslint error fix
rojiphil Oct 4, 2024
2a070f6
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Oct 4, 2024
5b133e8
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Oct 11, 2024
805eb47
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Oct 28, 2024
f9e2ddc
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Nov 20, 2024
4b60132
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Dec 9, 2024
56dac53
Merge branch 'Expensify:main' into new-places-api-support
rojiphil Dec 24, 2024
b2cb5b2
Merge branch 'main' of https://github.com/Expensify/App into new-plac…
rojiphil Jan 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
"react-native-draggable-flatlist": "^4.0.1",
"react-native-fs": "^2.20.0",
"react-native-gesture-handler": "2.20.1",
"react-native-google-places-autocomplete": "2.5.6",
"react-native-google-places-autocomplete": "2.5.7",
"react-native-haptic-feedback": "^2.3.3",
"react-native-image-picker": "^7.1.2",
"react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#cb392140db4953a283590d7cf93b4d0461baa2a9",
Expand Down
4 changes: 4 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1599,6 +1599,10 @@ const CONST = {

YOUR_LOCATION_TEXT: 'Your Location',

GOOGLE_PLACES_API: {
FIELDS_MASK: 'id,name,addressComponents,adrFormatAddress,formattedAddress,location,plusCode,shortFormattedAddress,types,viewport',
},

ATTACHMENT_MESSAGE_TEXT: '[Attachment]',
ATTACHMENT_SOURCE_ATTRIBUTE: 'data-expensify-source',
ATTACHMENT_OPTIMISTIC_SOURCE_ATTRIBUTE: 'data-optimistic-src',
Expand Down
54 changes: 27 additions & 27 deletions src/components/AddressSearch/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ function AddressSearch(
lat: 'addressLat',
lng: 'addressLng',
},
resultTypes = 'address',
shouldSaveDraft = false,
value,
locationBias,
Expand All @@ -98,26 +97,25 @@ function AddressSearch(
const containerRef = useRef<View>(null);
const query = useMemo(
() => ({
language: preferredLocale,
types: resultTypes,
components: isLimitedToUSA ? 'country:us' : undefined,
...(locationBias && {locationbias: locationBias}),
languageCode: preferredLocale,
includedRegionCodes: isLimitedToUSA ? ['us'] : undefined,
...(locationBias && {locationBias}),
}),
[preferredLocale, resultTypes, isLimitedToUSA, locationBias],
[preferredLocale, isLimitedToUSA, locationBias],
);
const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused;
const saveLocationDetails = (autocompleteData: GooglePlaceData, details: GooglePlaceDetail | null) => {
const addressComponents = details?.address_components;
const addressComponents = details?.addressComponents;
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 (details) {
onPress?.({
address: autocompleteData.description ?? '',
lat: details.geometry.location.lat ?? 0,
lng: details.geometry.location.lng ?? 0,
name: details.name,
lat: details.location?.latitude ?? details.geometry.location?.lat ?? 0,
lng: details.location?.longitude ?? details.geometry.location?.lng ?? 0,
name: autocompleteData?.structured_formatting?.main_text ?? details?.name ?? '',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

details.name isn't the same as it was in the old API, the format here is places/<id> so not really good to use here. The next closest thing is details?. shortFormattedAddress

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this context, the details.name is not used from old API. Instead, it indicates a predefined option i.e. saved places in our app. So, I think we need to keep
details.name

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you mean, can you show me? The way I understand this code, the value in autocompleteData.structured_formatting.main_text in the new API is most similar to details.name from the old api. However, details.name is no longer a good fallback, as in the new API, it does not contain a short version of the address, but something else entirely

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what you mean, can you show me?

Please find the test video that demonstrates the use of predefined places. This is the reason why I think we need to keep details.name.

47085-predefined-places.mp4

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm ok. Can you take a look at how this is utilized when changing your address on the profile settings page as well? I wonder if we need different values in the two places.

});
}
return;
Expand All @@ -137,27 +135,27 @@ function AddressSearch(
country: countryPrimary,
} = getAddressComponents(addressComponents, {
// eslint-disable-next-line @typescript-eslint/naming-convention
street_number: 'long_name',
route: 'long_name',
subpremise: 'long_name',
locality: 'long_name',
sublocality: 'long_name',
street_number: 'longText',
route: 'longText',
subpremise: 'longText',
locality: 'longText',
sublocality: 'longText',
// eslint-disable-next-line @typescript-eslint/naming-convention
postal_town: 'long_name',
postal_town: 'longText',
// eslint-disable-next-line @typescript-eslint/naming-convention
postal_code: 'long_name',
postal_code: 'longText',
// eslint-disable-next-line @typescript-eslint/naming-convention
administrative_area_level_1: 'short_name',
administrative_area_level_1: 'shortText',
// eslint-disable-next-line @typescript-eslint/naming-convention
administrative_area_level_2: 'long_name',
country: 'short_name',
administrative_area_level_2: 'longText',
country: 'shortText',
});

// The state's iso code (short_name) is needed for the StatePicker component but we also
// need the state's full name (long_name) when we render the state in a TextInput.
const {administrative_area_level_1: longStateName} = getAddressComponents(addressComponents, {
// eslint-disable-next-line @typescript-eslint/naming-convention
administrative_area_level_1: 'long_name',
administrative_area_level_1: 'longText',
});

// Make sure that the order of keys remains such that the country is always set above the state.
Expand All @@ -171,7 +169,7 @@ function AddressSearch(

const values = {
street: `${streetNumber} ${streetName}`.trim(),
name: details.name ?? '',
name: autocompleteData?.structured_formatting?.main_text ?? details?.name ?? '',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here as above

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

// Autocomplete returns any additional valid address fragments (e.g. Apt #) as subpremise.
street2: subpremise,
// Make sure country is updated first, since city and state will be reset if the country changes
Expand All @@ -184,9 +182,9 @@ function AddressSearch(
city: locality || postalTown || sublocality || cityAutocompleteFallback,
zipCode,

lat: details.geometry.location.lat ?? 0,
lng: details.geometry.location.lng ?? 0,
address: autocompleteData.description || details.formatted_address || '',
lat: details.location?.latitude ?? details.geometry.location?.lat ?? 0,
lng: details.location?.longitude ?? details.geometry.location?.lng ?? 0,
address: autocompleteData.description || details.formattedAddress || '',
};

// If the address is not in the US, use the full length state name since we're displaying the address's
Expand All @@ -208,9 +206,9 @@ function AddressSearch(

// Some edge-case addresses may lack both street_number and route in the API response, resulting in an empty "values.street"
// We are setting up a fallback to ensure "values.street" is populated with a relevant value
if (!values.street && details.adr_address) {
if (!values.street && details.adrFormatAddress) {
const streetAddressRegex = /<span class="street-address">([^<]*)<\/span>/;
const adrAddress = details.adr_address.match(streetAddressRegex);
const adrAddress = details.adrFormatAddress.match(streetAddressRegex);
const streetAddressFallback = adrAddress ? adrAddress?.[1] : null;
if (streetAddressFallback) {
values.street = streetAddressFallback;
Expand Down Expand Up @@ -475,6 +473,8 @@ function AddressSearch(
}
placeholder=""
listViewDisplayed
fields={CONST.GOOGLE_PLACES_API.FIELDS_MASK}
isNewPlacesAPI
>
<LocationErrorMessage
onClose={() => setLocationErrorCode(null)}
Expand Down
15 changes: 14 additions & 1 deletion src/components/AddressSearch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ type PredefinedPlace = Place & {
name?: string;
};

type LocationBias = {
rectangle: {
low: {
latitude: number;
longitude: number;
};
high: {
latitude: number;
longitude: number;
};
};
};

type AddressSearchProps = {
/** The ID used to uniquely identify the input in a Form */
inputID?: string;
Expand Down Expand Up @@ -82,7 +95,7 @@ type AddressSearchProps = {
resultTypes?: string;

/** Location bias for querying search results. */
locationBias?: string;
locationBias?: LocationBias | undefined;

/** Callback to be called when the country is changed */
onCountryChange?: (country: unknown) => void;
Expand Down
19 changes: 16 additions & 3 deletions src/hooks/useLocationBias.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,21 @@ export default function useLocationBias(allWaypoints: WaypointCollection, userLo
const north = maxLat < 90 ? maxLat : 90;
const east = maxLng < 180 ? maxLng : 180;

// Format: rectangle:south,west|north,east
const rectFormat = `rectangle:${south},${west}|${north},${east}`;
return rectFormat;
if (latitudes.length === 0 || longitudes.length === 0) {
return undefined;
}
const rectangularBoundary = {
rectangle: {
low: {
latitude: south,
longitude: west,
},
high: {
latitude: north,
longitude: east,
},
},
};
return rectangularBoundary;
}, [userLocation, allWaypoints]);
}
10 changes: 4 additions & 6 deletions src/libs/GooglePlacesUtils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
type AddressComponent = {
// eslint-disable-next-line @typescript-eslint/naming-convention
long_name: string;
// eslint-disable-next-line @typescript-eslint/naming-convention
short_name: string;
longText: string;
shortText: string;
types: string[];
};
type FieldsToExtract = Record<string, Exclude<keyof AddressComponent, 'types'>>;
Expand All @@ -11,8 +9,8 @@ type FieldsToExtract = Record<string, Exclude<keyof AddressComponent, 'types'>>;
* Finds an address component by type, and returns the value associated to key. Each address component object
* inside the addressComponents array has the following structure:
* [{
* long_name: "New York",
* short_name: "New York",
* longText: "New York",
* shortText: "New York",
* types: [ "locality", "political" ]
* }]
*/
Expand Down
64 changes: 23 additions & 41 deletions src/pages/iou/request/step/IOURequestStepWaypoint.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import type {TextInput} from 'react-native';
import {View} from 'react-native';
import type {Place} from 'react-native-google-places-autocomplete';
import {withOnyx} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import AddressSearch from '@components/AddressSearch';
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
Expand All @@ -20,11 +20,11 @@
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as ErrorUtils from '@libs/ErrorUtils';

Check failure on line 23 in src/pages/iou/request/step/IOURequestStepWaypoint.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"
import * as IOUUtils from '@libs/IOUUtils';

Check failure on line 24 in src/pages/iou/request/step/IOURequestStepWaypoint.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"
import Navigation from '@libs/Navigation/Navigation';
import * as ValidationUtils from '@libs/ValidationUtils';

Check failure on line 26 in src/pages/iou/request/step/IOURequestStepWaypoint.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Namespace imports from @libs are not allowed. Use named imports instead. Example: import { method } from "@libs/module"
import * as Transaction from '@userActions/Transaction';

Check failure on line 27 in src/pages/iou/request/step/IOURequestStepWaypoint.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Namespace imports from @userActions are not allowed. Use named imports instead. Example: import { action } from "@userActions/module"
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -37,26 +37,36 @@
import type {WithWritableReportOrNotFoundProps} from './withWritableReportOrNotFound';
import withWritableReportOrNotFound from './withWritableReportOrNotFound';

type IOURequestStepWaypointOnyxProps = {
/** List of recent waypoints */
recentWaypoints: OnyxEntry<Place[]>;

userLocation: OnyxEntry<OnyxTypes.UserLocation>;
type IOURequestStepWaypointProps = WithWritableReportOrNotFoundProps<typeof SCREENS.MONEY_REQUEST.STEP_WAYPOINT> & {
transaction: OnyxEntry<OnyxTypes.Transaction>;
};

type IOURequestStepWaypointProps = IOURequestStepWaypointOnyxProps &
WithWritableReportOrNotFoundProps<typeof SCREENS.MONEY_REQUEST.STEP_WAYPOINT> & {
transaction: OnyxEntry<OnyxTypes.Transaction>;
};
const recentWaypointsSelector = (waypoints: OnyxEntry<OnyxTypes.RecentWaypoint[]>): OnyxEntry<Place[]> =>
// Only grab the most recent 20 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.
(waypoints ? waypoints.slice(0, CONST.RECENT_WAYPOINTS_NUMBER as number) : [])
.filter((waypoint) => waypoint.keyForList?.includes(CONST.YOUR_LOCATION_TEXT) !== true)
.map((waypoint) => ({
name: waypoint.name,
description: waypoint.address ?? '',
geometry: {
location: {
lat: waypoint.lat ?? 0,
lng: waypoint.lng ?? 0,
latitude: waypoint.lat ?? 0,
longitude: waypoint.lng ?? 0,
},
},
})) as OnyxEntry<Place[]>;

function IOURequestStepWaypoint({
route: {
params: {action, backTo, iouType, pageIndex, reportID, transactionID},
},
transaction,
recentWaypoints = [],
userLocation,
}: IOURequestStepWaypointProps) {
const [userLocation] = useOnyx(ONYXKEYS.USER_LOCATION);
const [recentWaypoints] = useOnyx(ONYXKEYS.NVP_RECENT_WAYPOINTS, {selector: recentWaypointsSelector});
const styles = useThemeStyles();
const {windowWidth} = useWindowDimensions();
const [isDeleteStopModalOpen, setIsDeleteStopModalOpen] = useState(false);
Expand Down Expand Up @@ -247,32 +257,4 @@

IOURequestStepWaypoint.displayName = 'IOURequestStepWaypoint';

export default withWritableReportOrNotFound(
withFullTransactionOrNotFound(
withOnyx<IOURequestStepWaypointProps, IOURequestStepWaypointOnyxProps>({
userLocation: {
key: ONYXKEYS.USER_LOCATION,
},
recentWaypoints: {
key: ONYXKEYS.NVP_RECENT_WAYPOINTS,

// Only grab the most recent 20 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, CONST.RECENT_WAYPOINTS_NUMBER as number) : [])
.filter((waypoint) => waypoint.keyForList?.includes(CONST.YOUR_LOCATION_TEXT) !== true)
.map((waypoint) => ({
name: waypoint.name,
description: waypoint.address ?? '',
geometry: {
location: {
lat: waypoint.lat ?? 0,
lng: waypoint.lng ?? 0,
},
},
})),
},
})(IOURequestStepWaypoint),
),
true,
);
export default withWritableReportOrNotFound(withFullTransactionOrNotFound(IOURequestStepWaypoint));
Loading
Loading