diff --git a/README.md b/README.md index b05a392..923f868 100644 --- a/README.md +++ b/README.md @@ -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 + 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). @@ -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 diff --git a/index.d.ts b/index.d.ts index 5857591..840214d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -6,6 +6,7 @@ export interface ReactGoogleAutocompleteProps { ref: RefObject, autocompleteRef: RefObject ) => void; + onLoadFailed?: (error: Error | ErrorEvent) => void; inputAutocompleteValue?: string; options?: google.maps.places.AutocompleteOptions; libraries?: string[]; diff --git a/src/ReactGoogleAutocomplete.js b/src/ReactGoogleAutocomplete.js index 1d991ba..855ba6c 100644 --- a/src/ReactGoogleAutocomplete.js +++ b/src/ReactGoogleAutocomplete.js @@ -13,6 +13,7 @@ function ReactGoogleAutocomplete(props) { googleMapsScriptBaseUrl, refProp, language, + onLoadFailed, ...rest } = props; @@ -24,7 +25,8 @@ function ReactGoogleAutocomplete(props) { libraries, inputAutocompleteValue, options, - language + language, + onLoadFailed, }); return ; @@ -41,6 +43,7 @@ ReactGoogleAutocomplete.propTypes = { ]), googleMapsScriptBaseUrl: PropTypes.string, onPlaceSelected: PropTypes.func, + onLoadFailed: PropTypes.func, inputAutocompleteValue: PropTypes.string, options: PropTypes.shape({ componentRestrictions: PropTypes.object, diff --git a/src/usePlacesWidget.js b/src/usePlacesWidget.js index 4a0cebc..e12ec02 100644 --- a/src/usePlacesWidget.js +++ b/src/usePlacesWidget.js @@ -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); @@ -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(() => { diff --git a/src/utils.js b/src/utils.js index ccf6a6b..df0f329 100644 --- a/src/utils.js +++ b/src/utils.js @@ -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)); }); } @@ -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); }); };