import { BaseAddress } from '@bit-ui-libs/common'; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; import { useMemo } from 'react'; import { GCP_API_KEY } from '../core/config'; const PLACES_API_URL = 'https://maps.googleapis.com/maps/api/place'; export interface AddressComponent { long_name: string; short_name: string; types: string[]; } export interface PlaceDetails { result: { formatted_address: string; address_components: AddressComponent[]; name: string; }; } export interface Substring { length: number; offset: number; } export interface Term { offset: number; value: string; } export interface Prediction { description?: string; matched_substrings?: Substring[]; place_id?: string; reference?: string; structured_formatting: { main_text: string; main_text_matched_substrings?: Substring[]; }; address: { city: string; country: { code: string; name: string; }; state: { code: string; name: string; }; zip: string; }; terms?: Term[]; types?: string[]; name?: string; } export interface GoogleAutoComplete { predictions: Prediction[]; } export type BaseAddressFromPlaces = Omit; /** * Quick utility function to find a component in a Google Places Address object * based on the provided `types` (e.g. 'administrative_area_level_1', 'country', etc.) * * @param components Google Places address component * @param types List of types (strings) that we want to extract from the Google Places address * @param field Type of name (either long or short name) that we extract if we found the component * @returns */ function findComponent( components: AddressComponent[], types: string[], field: keyof Omit = 'long_name' ) { const component = components.find((c) => c.types.some((t) => types.includes(t))); return component ? component[field] : null; } /** * Utility function that transforms "Google Places API Details" * to an easily consumable address object (BaseAddress) * * @param {PlaceDetails} details Google Places API Details * @returns {BaseAddress} an Address object */ function placeToAddress(details: PlaceDetails): BaseAddressFromPlaces { const { formatted_address, address_components } = details.result; const country = findComponent(address_components, ['country']); const state = findComponent(address_components, ['administrative_area_level_1']); const city = findComponent(address_components, ['locality']); const zip = findComponent(address_components, ['postal_code']); // We don't seem to be using this anyway // const streetNumber = findComponent(address_components, ['street_number']); // const streetName = findComponent(address_components, ['route']); return { address: formatted_address, city: city ?? '', country: country ?? '', state: state ?? '', zip: zip ?? '', }; } interface UseGoogleAddressOptions { search?: string; } /** * Hook for handling Google Autocomplete & Places API integration. * * @param options Pass `search` string here * @returns `react-query`-like payload */ export function useGoogleAddress(options: UseGoogleAddressOptions) { const { search } = options; const axiosClient = useMemo(() => axios.create({ baseURL: PLACES_API_URL }), []); return useQuery({ queryKey: ['google-address', search], queryFn: async () => { // Based on `search`, make one Google Autocomplete API call // to get predictions of the address that the user is searching for const { data } = await axiosClient.get('/autocomplete/json', { params: { key: GCP_API_KEY, input: search, language: 'en', }, }); // For each prediction, make additional Google Places Details API call // to get more specific information (specifically `address_components`) about the address. return Promise.all( data.predictions.map(async (prediction) => { const { data } = await axiosClient.get('/details/json', { params: { key: GCP_API_KEY, fields: 'address_components,formatted_address,geometry,name', placeid: prediction.place_id, language: 'en', }, }); // After getting the "Place Details", // convert data to our custom `BaseAddress` interface for easy consumption. return placeToAddress(data); }) ); }, enabled: !!options?.search, }); }