Skip to content

Commit

Permalink
feat: handle API-key errors in map-component
Browse files Browse the repository at this point in the history
Introduces handling for API-key errors (via the gm_authFailed callback) for the map-component.

This also shifts responsibility for tracking the loading status into the GoogleMapsApiLoader class, to avoid problems when the APIProvider itself is rendered conditionally.
  • Loading branch information
usefulthink committed Jan 18, 2024
1 parent 7309efa commit a15198f
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 32 deletions.
38 changes: 17 additions & 21 deletions src/components/api-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,12 @@ import React, {
useState
} from 'react';

import {GoogleMapsApiLoader} from '../libraries/google-maps-api-loader';
import {
APILoadingStatus,
GoogleMapsApiLoader
} from '../libraries/google-maps-api-loader';

export enum APILoadingStatus {
NOT_LOADED = 'NOT_LOADED',
LOADING = 'LOADING',
LOADED = 'LOADED',
FAILED = 'FAILED'
}

const {NOT_LOADED, LOADING, LOADED, FAILED} = APILoadingStatus;
export {APILoadingStatus} from '../libraries/google-maps-api-loader';

type ImportLibraryFunction = typeof google.maps.importLibrary;
type GoogleMapsLibrary = Awaited<ReturnType<ImportLibraryFunction>>;
Expand Down Expand Up @@ -77,7 +73,7 @@ export type APIProviderProps = {
};

/**
* local hook to manage access to map-instances.
* local hook to set up the map-instance management context.
*/
function useMapInstances() {
const [mapInstances, setMapInstances] = useState<
Expand Down Expand Up @@ -107,7 +103,9 @@ function useMapInstances() {
function useGoogleMapsApiLoader(props: APIProviderProps) {
const {onLoad, apiKey, libraries = [], ...otherApiParams} = props;

const [status, setStatus] = useState<APILoadingStatus>(NOT_LOADED);
const [status, setStatus] = useState<APILoadingStatus>(
GoogleMapsApiLoader.loadingStatus
);
const [loadedLibraries, addLoadedLibrary] = useReducer(
(
loadedLibraries: LoadedLibraries,
Expand Down Expand Up @@ -147,17 +145,16 @@ function useGoogleMapsApiLoader(props: APIProviderProps) {

useEffect(
() => {
setStatus(LOADING);

(async () => {
try {
await GoogleMapsApiLoader.load({
key: apiKey,
libraries: librariesString,
...otherApiParams
});

setStatus(LOADED);
await GoogleMapsApiLoader.load(
{
key: apiKey,
libraries: librariesString,
...otherApiParams
},
status => setStatus(status)
);

for (const name of ['core', 'maps', ...libraries]) {
await importLibrary(name);
Expand All @@ -168,7 +165,6 @@ function useGoogleMapsApiLoader(props: APIProviderProps) {
}
} catch (error) {
console.error('<ApiProvider> failed to load Google Maps API', error);
setStatus(FAILED);
}
})();
},
Expand Down
48 changes: 47 additions & 1 deletion src/components/map/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import React, {
useState
} from 'react';

import {APIProviderContext, APIProviderContextValue} from '../api-provider';
import {
APILoadingStatus,
APIProviderContext,
APIProviderContextValue
} from '../api-provider';

import {useApiIsLoaded} from '../../hooks/use-api-is-loaded';
import {logErrorOnce} from '../../libraries/errors';
Expand All @@ -18,6 +22,7 @@ import {MapEventProps, useMapEvents} from './use-map-events';
import {useMapOptions} from './use-map-options';
import {useDeckGLCameraUpdate} from './use-deckgl-camera-update';
import {useInternalCameraState} from './use-internal-camera-state';
import {useApiLoadingStatus} from '../../hooks/use-api-loading-status';

export interface GoogleMapsContextValue {
map: google.maps.Map | null;
Expand Down Expand Up @@ -66,6 +71,7 @@ export const Map = (props: PropsWithChildren<MapProps>) => {
const {children, id, className, style, viewState, viewport} = props;

const context = useContext(APIProviderContext);
const loadingStatus = useApiLoadingStatus();

if (!context) {
throw new Error(
Expand All @@ -92,6 +98,16 @@ export const Map = (props: PropsWithChildren<MapProps>) => {
[style, isViewportSet]
);

if (loadingStatus === APILoadingStatus.AUTH_FAILURE) {
return (
<div
style={{position: 'relative', ...(className ? {} : combinedStyle)}}
className={className}>
<AuthFailureMessage />
</div>
);
}

return (
<div
ref={mapRef}
Expand All @@ -109,6 +125,36 @@ export const Map = (props: PropsWithChildren<MapProps>) => {
};
Map.deckGLViewProps = true;

const AuthFailureMessage = () => {
const style: CSSProperties = {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
zIndex: 999,
display: 'flex',
flexFlow: 'column nowrap',
textAlign: 'center',
justifyContent: 'center',
fontSize: '.8rem',
color: 'rgba(0,0,0,0.6)',
background: '#dddddd',
padding: '1rem 1.5rem'
};

return (
<div style={style}>
<h2>Error: AuthFailure</h2>
<p>
A problem with your API key prevents the map from rendering correctly.
Please make sure the value of the <code>APIProvider.apiKey</code> prop
is correct. Check the error-message in the console for further details.
</p>
</div>
);
};

/**
* The main hook takes care of creating map-instances and registering them in
* the api-provider context.
Expand Down
60 changes: 50 additions & 10 deletions src/libraries/google-maps-api-loader.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
export enum APILoadingStatus {
NOT_LOADED = 'NOT_LOADED',
LOADING = 'LOADING',
LOADED = 'LOADED',
FAILED = 'FAILED',
AUTH_FAILURE = 'AUTH_FAILURE'
}

export type ApiParams = {
key: string;
v?: string;
Expand All @@ -12,7 +20,7 @@ export type ApiParams = {
declare global {
interface Window {
__googleMapsCallback__?: () => void;
__googleMapsApiParams__?: string;
gm_authFailure?: () => void;
}
}

Expand All @@ -25,6 +33,9 @@ const MAPS_API_BASE_URL = 'https://maps.googleapis.com/maps/api/js';
* allow using the API in an useEffect hook, without worrying about multiple API loads.
*/
export class GoogleMapsApiLoader {
public static loadingStatus: APILoadingStatus = APILoadingStatus.NOT_LOADED;
public static serializedApiParams?: string;

/**
* Loads the Google Maps API with the specified parameters.
* Since the Maps library can only be loaded once per page, this will
Expand All @@ -33,24 +44,35 @@ export class GoogleMapsApiLoader {
*
* The returned promise resolves when loading completes
* and rejects in case of an error or when the loading was aborted.
* @param params
*/
static async load(params: ApiParams): Promise<void> {
static async load(
params: ApiParams,
onLoadingStatusChange: (status: APILoadingStatus) => void
): Promise<void> {
const libraries = params.libraries ? params.libraries.split(',') : [];
const serializedParams = this.serializeParams(params);

// note: if google.maps.importLibrary was defined externally, the params
// will be ignored. If it was defined by a previous call to this
// method, we will check that the key and other parameters have not been
// changed in between calls.

if (!window.google?.maps?.importLibrary) {
window.__googleMapsApiParams__ = serializedParams;
this.initImportLibrary(params);
this.serializedApiParams = serializedParams;
this.initImportLibrary(params, onLoadingStatusChange);
} else {
// if serializedApiParams isn't defined the library was loaded externally
// and we can only assume that went alright.
if (!this.serializedApiParams) {
this.loadingStatus = APILoadingStatus.LOADED;
}

onLoadingStatusChange(this.loadingStatus);
}

if (
window.__googleMapsApiParams__ &&
window.__googleMapsApiParams__ !== serializedParams
this.serializedApiParams &&
this.serializedApiParams !== serializedParams
) {
console.warn(
`The maps API has already been loaded with different ` +
Expand All @@ -75,7 +97,10 @@ export class GoogleMapsApiLoader {
].join('/');
}

private static initImportLibrary(params: ApiParams) {
private static initImportLibrary(
params: ApiParams,
onLoadingStatusChange: (status: APILoadingStatus) => void
) {
if (!window.google) window.google = {} as never;
if (!window.google.maps) window.google.maps = {} as never;

Expand Down Expand Up @@ -105,14 +130,29 @@ export class GoogleMapsApiLoader {
urlParams.set('callback', '__googleMapsCallback__');
scriptElement.src = MAPS_API_BASE_URL + `?` + urlParams.toString();

window.__googleMapsCallback__ = resolve;
window.__googleMapsCallback__ = () => {
this.loadingStatus = APILoadingStatus.LOADED;
onLoadingStatusChange(this.loadingStatus);
resolve();
};

window.gm_authFailure = () => {
this.loadingStatus = APILoadingStatus.AUTH_FAILURE;
onLoadingStatusChange(this.loadingStatus);
};

scriptElement.onerror = () =>
scriptElement.onerror = () => {
this.loadingStatus = APILoadingStatus.FAILED;
onLoadingStatusChange(this.loadingStatus);
reject(new Error('The Google Maps JavaScript API could not load.'));
};

scriptElement.nonce =
(document.querySelector('script[nonce]') as HTMLScriptElement)
?.nonce || '';

this.loadingStatus = APILoadingStatus.LOADING;
onLoadingStatusChange(this.loadingStatus);
document.head.append(scriptElement);
});

Expand Down

0 comments on commit a15198f

Please sign in to comment.