Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@ import Autocomplete from "react-google-autocomplete";

- `onPlaceSelected: (place: `[PlaceResult](https://developers.google.com/maps/documentation/javascript/reference/places-service#PlaceResult)`, inputRef, `[autocompleteRef](https://developers.google.com/maps/documentation/javascript/reference/places-widget#Autocomplete)`) => void`: The function gets invoked every time a user chooses location.

- `onLoadFailed: (error: Error | ErrorEvent) => void`: Optional. The function gets invoked when the Google Maps script fails to load (e.g. it is blocked by an ad-blocker or CSP, the network is down, or a CDN/DNS error occurs) or when Google reports an authentication/quota failure via its global `gm_authFailure` callback (invalid API key, disabled billing, referrer restrictions, exceeded quota). When omitted, behavior is unchanged.

```js
<ReactGoogleAutocomplete
apiKey={YOUR_GOOGLE_MAPS_API_KEY}
onPlaceSelected={(place) => console.log(place)}
onLoadFailed={(error) => console.error("Could not load Google Maps", error)}
/>
```

- `options`: [Google autocomplete options.](https://developers.google.com/maps/documentation/javascript/reference/places-widget#AutocompleteOptions)

- `options.types`: By default it uses (cities).
Expand Down Expand Up @@ -112,7 +122,7 @@ export default () => {

### Arguments

It has only one config argument which has propperties: `apiKey`, `ref`, `onPlaceSelected`, `options`, `inputAutocompleteValue`, `googleMapsScriptBaseUrl`. The same props described [here](#reactgoogleautocomplete)
It has only one config argument which has propperties: `apiKey`, `ref`, `onPlaceSelected`, `onLoadFailed`, `options`, `inputAutocompleteValue`, `googleMapsScriptBaseUrl`. The same props described [here](#reactgoogleautocomplete)

### Returned value

Expand Down
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ReactGoogleAutocompleteProps {
ref: RefObject<HTMLInputElement>,
autocompleteRef: RefObject<google.maps.places.Autocomplete>
) => void;
onLoadFailed?: (error: Error | ErrorEvent) => void;
inputAutocompleteValue?: string;
options?: google.maps.places.AutocompleteOptions;
libraries?: string[];
Expand Down
5 changes: 4 additions & 1 deletion src/ReactGoogleAutocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ function ReactGoogleAutocomplete(props) {
googleMapsScriptBaseUrl,
refProp,
language,
onLoadFailed,
...rest
} = props;

Expand All @@ -24,7 +25,8 @@ function ReactGoogleAutocomplete(props) {
libraries,
inputAutocompleteValue,
options,
language
language,
onLoadFailed,
});

return <input ref={ref} {...rest} />;
Expand All @@ -41,6 +43,7 @@ ReactGoogleAutocomplete.propTypes = {
]),
googleMapsScriptBaseUrl: PropTypes.string,
onPlaceSelected: PropTypes.func,
onLoadFailed: PropTypes.func,
inputAutocompleteValue: PropTypes.string,
options: PropTypes.shape({
componentRestrictions: PropTypes.object,
Expand Down
32 changes: 30 additions & 2 deletions src/usePlacesWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export default function usePlacesWidget(props) {
} = {},
googleMapsScriptBaseUrl = GOOGLE_MAP_SCRIPT_BASE_URL,
language,
onLoadFailed,
} = props;
const inputRef = useRef(null);
const event = useRef(null);
Expand Down Expand Up @@ -85,13 +86,40 @@ export default function usePlacesWidget(props) {
}
};

// Google only surfaces auth/quota failures (invalid key, disabled billing,
// referrer not allowed, over quota) through the global gm_authFailure
// callback. When the consumer opts in via onLoadFailed, chain any
// pre-existing handler so we don't clobber it, and restore it on cleanup.
const previousAuthFailure = isBrowser ? window.gm_authFailure : undefined;

if (isBrowser && onLoadFailed) {
window.gm_authFailure = (...args) => {
if (typeof previousAuthFailure === "function")
previousAuthFailure(...args);
onLoadFailed(
new Error(
"Google Maps authentication failed. This may be caused by an invalid API key, disabled billing, referrer restrictions, or exceeded quota."
)
);
};
}

if (apiKey) {
handleLoadScript().then(() => handleAutoComplete());
handleLoadScript()
.then(() => handleAutoComplete())
// always attach a catch so a rejected load never becomes an unhandled
// promise rejection; forward it to the consumer when they opted in
.catch((error) => {
if (onLoadFailed) onLoadFailed(error);
});
} else {
handleAutoComplete();
}

return () => (event.current ? event.current.remove() : undefined);
return () => {
if (event.current) event.current.remove();
if (isBrowser && onLoadFailed) window.gm_authFailure = previousAuthFailure;
};
}, []);

useEffect(() => {
Expand Down
11 changes: 9 additions & 2 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ export const loadGoogleMapScript = (
);

if (scriptElements && scriptElements.length) {
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
// in case we already have a script on the page and it's loaded we resolve
if (typeof google !== "undefined") return resolve();

// otherwise we wait until it's loaded and resolve
scriptElements[0].addEventListener("load", () => resolve());

// or reject if it fails to load (e.g. blocked, network/CDN error)
scriptElements[0].addEventListener("error", (error) => reject(error));
});
}

Expand All @@ -32,8 +35,12 @@ export const loadGoogleMapScript = (
const el = document.createElement("script");
el.src = scriptUrl.toString();

return new Promise((resolve) => {
return new Promise((resolve, reject) => {
window.__REACT_GOOGLE_AUTOCOMPLETE_CALLBACK__ = resolve;

// reject if the script fails to load (e.g. blocked, network/CDN error)
el.addEventListener("error", (error) => reject(error));

document.body.appendChild(el);
});
};